Compare commits

...

15 Commits

Author SHA1 Message Date
Raz 9b80437554 add u18 management 10 months ago
Raz f4718b8010 wip 10 months ago
Raz 7dc2619543 wip 10 months ago
Raz 472a456465 wip 10 months ago
Raz a206c351c3 fix issue with locationbutton 10 months ago
Raz 7a2cf4edea wip 10 months ago
Raz bd2ba72560 wip 10 months ago
Raz 354ae4c527 wip 10 months ago
Raz a6b4b46afa wip fix slow 10 months ago
Raz 28a0536248 fix progressview 11 months ago
Raz 7337767dd2 wip 11 months ago
Raz 99bf6c40d4 fix 11 months ago
Raz a2f0ef63e1 fix 11 months ago
Raz 7a71beeea8 fix issues 11 months ago
Raz 59fe22662b paca chpship 11 months ago
  1. 36
      PadelClub.xcodeproj/project.pbxproj
  2. 8
      PadelClub/Data/GroupStage.swift
  3. 8
      PadelClub/Data/Match.swift
  4. 182
      PadelClub/Data/PlayerRegistration.swift
  5. 24
      PadelClub/Data/Round.swift
  6. 134
      PadelClub/Data/TeamRegistration.swift
  7. 210
      PadelClub/Data/Tournament.swift
  8. 36
      PadelClub/Extensions/String+Extensions.swift
  9. 5
      PadelClub/Utils/ExportFormat.swift
  10. 406
      PadelClub/Utils/FileImportManager.swift
  11. 21
      PadelClub/Utils/PadelRule.swift
  12. 4
      PadelClub/Utils/SwiftParser.swift
  13. 30
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  14. 1
      PadelClub/Views/Navigation/MainView.swift
  15. 17
      PadelClub/Views/Navigation/Umpire/PadelClubView.swift
  16. 2
      PadelClub/Views/Round/LoserRoundView.swift
  17. 10
      PadelClub/Views/Round/LoserRoundsView.swift
  18. 9
      PadelClub/Views/Team/Components/TeamWeightView.swift
  19. 3
      PadelClub/Views/Team/TeamRowView.swift
  20. 23
      PadelClub/Views/Tournament/FileImportView.swift
  21. 37
      PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift
  22. 231
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -3278,7 +3278,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3291,7 +3291,10 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3302,8 +3305,8 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.34; MARKETING_VERSION = 1.0.42;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.dec;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -3323,7 +3326,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3335,7 +3338,10 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3346,8 +3352,8 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.34; MARKETING_VERSION = 1.0.42;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.dec;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -3450,8 +3456,12 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3493,8 +3503,12 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3537,6 +3551,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3578,6 +3593,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;

@ -301,7 +301,7 @@ final class GroupStage: ModelObject, Storable {
} }
func availableToStart(playedMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { func availableToStart(playedMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -312,7 +312,7 @@ final class GroupStage: ModelObject, Storable {
} }
func runningMatches(playedMatches: [Match]) -> [Match] { func runningMatches(playedMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -323,7 +323,7 @@ final class GroupStage: ModelObject, Storable {
} }
func readyMatches(playedMatches: [Match]) -> [Match] { func readyMatches(playedMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -334,7 +334,7 @@ final class GroupStage: ModelObject, Storable {
} }
func finishedMatches(playedMatches: [Match]) -> [Match] { func finishedMatches(playedMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)

@ -108,7 +108,7 @@ final class Match: ModelObject, Storable, Equatable {
} }
func indexInRound(in matches: [Match]? = nil) -> Int { func indexInRound(in matches: [Match]? = nil) -> Int {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -132,7 +132,7 @@ defer {
} }
func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String { func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -808,7 +808,7 @@ defer {
} }
func teams() -> [TeamRegistration] { func teams() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -883,7 +883,7 @@ defer {
} }
func team(_ team: TeamPosition) -> TeamRegistration? { func team(_ team: TeamPosition) -> TeamRegistration? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)

@ -41,9 +41,16 @@ final class PlayerRegistration: ModelObject, Storable {
var coach: Bool = false var coach: Bool = false
var captain: Bool = false var captain: Bool = false
var clubCode: String?
var sourceName: String?
var isNveq: Bool = false
var isEQConfirmed: Bool?
var duplicatePlayers: Int?
var isYearValid: Bool?
func localizedSourceLabel() -> String { func localizedSourceLabel() -> String {
switch source { switch source {
case .frenchFederation: case .frenchFederation, .frenchFederationVerified, .frenchFederationEQVerified:
return "Via la base fédérale" return "Via la base fédérale"
case .beachPadel: case .beachPadel:
return "Via le fichier beach-padel" return "Via le fichier beach-padel"
@ -159,13 +166,120 @@ final class PlayerRegistration: ModelObject, Storable {
return nil 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 { func pasteData(_ exportFormat: ExportFormat = .rawText) -> String {
switch exportFormat { switch exportFormat {
case .rawText: case .rawText:
return [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: exportFormat.separator()) return [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: exportFormat.separator())
case .csv: case .csv:
return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator()) return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator())
case .championship:
let values = [
lastName.uppercased(),
firstName.capitalized,
isVerified(),
"=\"" + formattedLicense() + "\"",
"\(computedRank)",
isNveq ? "NVEQ" : "EQ",
]
.joined(separator: exportFormat.separator())
return values
}
}
func isVerified() -> String {
switch source {
case .frenchFederationVerified:
return "ok"
case .frenchFederationEQVerified:
return "ok2"
default:
return ""
}
}
func championshipAlerts(tournament: Tournament, allPlayers: [(String, String)], forRegional: Bool) -> [ChampionshipAlert] {
var alerts = [ChampionshipAlert]()
if forRegional, source != .frenchFederationEQVerified {
if isNveq == false && (isEQConfirmed == false || isEQConfirmed == nil) {
alerts.append(.notEQ(isEQConfirmed, self))
}
}
if isYearValid == nil || isYearValid == false {
alerts.append(.isYearValid(isYearValid, self))
}
if duplicatePlayers == nil {
let teamClubCode = self.team()?.clubCode
let strippedLicense = self.licenceId?.strippedLicense
duplicatePlayers = allPlayers.count(where: { strippedLicense == $0.0 && teamClubCode != $0.1 })
}
if let duplicatePlayers {
if duplicatePlayers > 0 {
print("doublon found \(duplicatePlayers)")
alerts.append(.duplicate(duplicatePlayers, self))
}
}
if isUnranked() && source == nil {
alerts.append(.unranked(self))
} else if source != .frenchFederationVerified && source != .frenchFederationEQVerified {
if tournament.federalTournamentAge == .senior {
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 isClubCodeOK() -> Bool {
team()?.clubCode?.trimmed.canonicalVersion == clubCode?.trimmed.canonicalVersion
}
func isLicenceOK() -> Bool {
guard let licenceId else { return false }
let licenceIdTrimmed = licenceId.trimmed
guard licenceIdTrimmed.strippedLicense != nil else { return false }
if licenceIdTrimmed.hasLicenseKey() {
return licenceIdTrimmed.isLicenseNumber
} else {
return true
}
}
func isNameOK() -> Bool {
lastName.canonicalVersion == sourceName?.canonicalVersion
} }
func isPlaying() -> Bool { func isPlaying() -> Bool {
@ -256,9 +370,34 @@ final class PlayerRegistration: ModelObject, Storable {
return "non classé" + (isMalePlayer() ? "" : "e") return "non classé" + (isMalePlayer() ? "" : "e")
} }
} }
@MainActor func verifyEQ(from sources: [CSVParser]) async throws {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if let dataFound = try await history(from: sources) {
print("dataFound.clubCode == clubCode", dataFound.clubCode, clubCode)
isEQConfirmed = dataFound.clubCode == clubCode
} else {
print("not found")
isEQConfirmed = nil
}
}
func updateRank(from sources: [CSVParser], lastRank: Int) async throws { func updateRank(from sources: [CSVParser], lastRank: Int) async throws {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if let dataFound = try await history(from: sources) { if let dataFound = try await history(from: sources) {
rank = dataFound.rankValue?.toInt() rank = dataFound.rankValue?.toInt()
points = dataFound.points points = dataFound.points
@ -269,6 +408,14 @@ final class PlayerRegistration: ModelObject, Storable {
} }
func history(from sources: [CSVParser]) async throws -> Line? { func history(from sources: [CSVParser]) async throws -> Line? {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func history()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
guard let license = licenceId?.strippedLicense else { guard let license = licenceId?.strippedLicense else {
return try await historyFromName(from: sources) return try await historyFromName(from: sources)
} }
@ -294,6 +441,14 @@ final class PlayerRegistration: ModelObject, Storable {
} }
func historyFromName(from sources: [CSVParser]) async throws -> Line? { func historyFromName(from sources: [CSVParser]) async throws -> Line? {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func historyFromName()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return await withTaskGroup(of: Line?.self) { group in return await withTaskGroup(of: Line?.self) { group in
for source in sources.filter({ $0.maleData == isMalePlayer() }) { for source in sources.filter({ $0.maleData == isMalePlayer() }) {
group.addTask { [lastName, firstName] in group.addTask { [lastName, firstName] in
@ -380,6 +535,12 @@ final class PlayerRegistration: ModelObject, Storable {
case _hasArrived = "hasArrived" case _hasArrived = "hasArrived"
case _coach = "coach" case _coach = "coach"
case _captain = "captain" case _captain = "captain"
case _clubCode = "clubCode"
case _sourceName = "sourceName"
case _isNveq = "isNveq"
case _duplicatePlayers = "duplicatePlayers"
case _isYearValid = "isYearValid"
case _isEQConfirmed = "isEQConfirmed"
} }
@ -410,6 +571,13 @@ final class PlayerRegistration: ModelObject, Storable {
email = try container.decodeIfPresent(String.self, forKey: ._email) email = try container.decodeIfPresent(String.self, forKey: ._email)
birthdate = try container.decodeIfPresent(String.self, forKey: ._birthdate) birthdate = try container.decodeIfPresent(String.self, forKey: ._birthdate)
source = try container.decodeIfPresent(PlayerDataSource.self, forKey: ._source) 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)
isEQConfirmed = try container.decodeIfPresent(Bool.self, forKey: ._isEQConfirmed)
duplicatePlayers = try container.decodeIfPresent(Int.self, forKey: ._duplicatePlayers)
isYearValid = try container.decodeIfPresent(Bool.self, forKey: ._isYearValid)
sourceName = try container.decodeIfPresent(String.self, forKey: ._sourceName)
} }
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
@ -437,12 +605,20 @@ final class PlayerRegistration: ModelObject, Storable {
try container.encode(hasArrived, forKey: ._hasArrived) try container.encode(hasArrived, forKey: ._hasArrived)
try container.encode(captain, forKey: ._captain) try container.encode(captain, forKey: ._captain)
try container.encode(coach, forKey: ._coach) try container.encode(coach, forKey: ._coach)
try container.encode(clubCode, forKey: ._clubCode)
try container.encode(sourceName, forKey: ._sourceName)
try container.encode(isNveq, forKey: ._isNveq)
try container.encode(isEQConfirmed, forKey: ._isEQConfirmed)
try container.encode(duplicatePlayers, forKey: ._duplicatePlayers)
try container.encode(isYearValid, forKey: ._isYearValid)
} }
enum PlayerDataSource: Int, Codable { enum PlayerDataSource: Int, Codable {
case frenchFederation = 0 case frenchFederation = 0
case beachPadel = 1 case beachPadel = 1
case onlineRegistration = 2 case onlineRegistration = 2
case frenchFederationVerified = 3
case frenchFederationEQVerified = 4
} }
enum PlayerSexType: Int, Hashable, CaseIterable, Identifiable, Codable { enum PlayerSexType: Int, Hashable, CaseIterable, Identifiable, Codable {

@ -175,7 +175,7 @@ final class Round: ModelObject, Storable {
} }
func roundProjectedTeam(_ team: TeamPosition, inMatch match: Match, previousRound: Round?) -> TeamRegistration? { func roundProjectedTeam(_ team: TeamPosition, inMatch match: Match, previousRound: Round?) -> TeamRegistration? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -217,7 +217,7 @@ defer {
} }
func upperBracketTopMatch(ofMatchIndex matchIndex: Int, previousRound: Round?) -> Match? { func upperBracketTopMatch(ofMatchIndex matchIndex: Int, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -237,7 +237,7 @@ defer {
} }
func upperBracketBottomMatch(ofMatchIndex matchIndex: Int, previousRound: Round?) -> Match? { func upperBracketBottomMatch(ofMatchIndex matchIndex: Int, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -259,7 +259,7 @@ defer {
func topPreviousRoundMatch(ofMatch match: Match, previousRound: Round?) -> Match? { func topPreviousRoundMatch(ofMatch match: Match, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -275,7 +275,7 @@ defer {
} }
func bottomPreviousRoundMatch(ofMatch match: Match, previousRound: Round?) -> Match? { func bottomPreviousRoundMatch(ofMatch match: Match, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -302,7 +302,7 @@ defer {
} }
// func displayableMatches() -> [Match] { // func displayableMatches() -> [Match] {
//#if _DEBUG_TIME //DEBUGING TIME //#if DEBUG //DEBUGING TIME
// let start = Date() // let start = Date()
// defer { // defer {
// let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) // let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -328,7 +328,7 @@ defer {
} }
func previousRound() -> Round? { func previousRound() -> Round? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -462,7 +462,7 @@ defer {
func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String { func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let _cachedSeedInterval { return _cachedSeedInterval.localizedLabel(displayStyle) } if let _cachedSeedInterval { return _cachedSeedInterval.localizedLabel(displayStyle) }
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -513,7 +513,7 @@ defer {
func seedInterval(initialMode: Bool = false) -> SeedInterval? { func seedInterval(initialMode: Bool = false) -> SeedInterval? {
if initialMode == false, let _cachedSeedInterval { return _cachedSeedInterval } if initialMode == false, let _cachedSeedInterval { return _cachedSeedInterval }
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -607,7 +607,7 @@ defer {
func loserRounds() -> [Round] { func loserRounds() -> [Round] {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -801,7 +801,7 @@ extension Round: Selectable, Equatable {
} }
func badgeValue() -> Int? { func badgeValue() -> Int? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -822,7 +822,7 @@ extension Round: Selectable, Equatable {
} }
func badgeImage() -> Badge? { func badgeImage() -> Badge? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)

@ -42,6 +42,10 @@ final class TeamRegistration: ModelObject, Storable {
var unregistered: Bool = false var unregistered: Bool = false
var unregistrationDate: Date? = nil var unregistrationDate: Date? = nil
var clubCode: String?
var registratonMail: String?
var clubName: String?
func hasUnregistered() -> Bool { func hasUnregistered() -> Bool {
unregistered unregistered
} }
@ -86,7 +90,7 @@ final class TeamRegistration: ModelObject, Storable {
// MARK: - Computed dependencies // MARK: - Computed dependencies
func unsortedPlayers() -> [PlayerRegistration] { 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: - // MARK: -
@ -375,15 +379,116 @@ final class TeamRegistration: ModelObject, Storable {
resetBracketPosition() resetBracketPosition()
} }
func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String { func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0, allPlayers: [(String, String)] = []) -> String {
switch exportFormat { switch exportFormat {
case .rawText: case .rawText:
return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name].compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator()) return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name].compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
case .csv: case .csv:
return [index.formatted(), playersPasteData(exportFormat), isWildCard() ? "WC" : weight.formatted()].joined(separator: exportFormat.separator()) return [index.formatted(), playersPasteData(exportFormat), isWildCard() ? "WC" : weight.formatted()].joined(separator: exportFormat.separator())
case .championship:
var baseValue: [String] = [
formattedInscriptionDate(exportFormat) ?? "",
alertCountFormatted(teamIndex: index, allPlayers: allPlayers),
alertDescription(teamIndex: index, allPlayers: allPlayers),
teamWeightFormatted(),
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 teamWeightFormatted() -> String {
let value = players().prefix(6).map({ $0.computedRank }).reduce(0,+)
return "\(value)"
}
func unrankedCountFormatted() -> String {
players().filter({ $0.isUnranked() && $0.source == nil }).count.formatted()
}
func alertDescription(teamIndex: Int, allPlayers: [(String, String)]) -> String {
let multiLineString = championshipAlerts(teamIndex: teamIndex, tournament: tournamentObject()!, allPlayers: allPlayers).compactMap({ $0.errorDescription }).joined(separator: "\n")
let escapedString = "\"\(multiLineString.replacingOccurrences(of: "\"", with: "\"\""))\""
return escapedString
}
func championshipAlerts(teamIndex: Int, tournament: Tournament, allPlayers: [(String, String)]) -> [ChampionshipAlert] {
var alerts = [ChampionshipAlert]()
if clubCode?.isValidCodeClub(62) == false {
alerts.append(.clubCodeInvalid(self))
}
let players = players()
if teamIndex <= 24, players.filter({ $0.isNveq }).count > 2 {
alerts.append(.tooManyNVEQ(self))
}
// if teamIndex <= 24, players.count > 8 {
// alerts.append(.tooManyPlayers(self))
//
// }
players.forEach { pr in
alerts.append(contentsOf: pr.championshipAlerts(tournament: tournament, allPlayers: allPlayers, forRegional: teamIndex <= 24))
} }
return alerts
}
func jokerWeightFormatted() -> String {
if let joker = players()[safe:5] {
return "\(joker.computedRank)"
} else {
return ""
}
}
func alertCountFormatted(teamIndex: Int, allPlayers: [(String, String)]) -> String {
let championshipAlertsCount = championshipAlerts(teamIndex: teamIndex, tournament: tournamentObject()!, allPlayers: allPlayers).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 { var computedRegistrationDate: Date {
return registrationDate ?? .distantFuture return registrationDate ?? .distantFuture
} }
@ -402,6 +507,15 @@ final class TeamRegistration: ModelObject, Storable {
} else { } else {
return nil return nil
} }
case .championship:
if let registrationDate {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter.string(from: registrationDate)
} else {
return nil
}
} }
} }
@ -414,7 +528,7 @@ final class TeamRegistration: ModelObject, Storable {
} else { } else {
return nil return nil
} }
case .csv: case .csv, .championship:
if let callDate { if let callDate {
return callDate.formatted(.dateTime.weekday().day().month().hour().minute()) return callDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else { } else {
@ -429,6 +543,8 @@ final class TeamRegistration: ModelObject, Storable {
return players().map { $0.pasteData(exportFormat) }.joined(separator: exportFormat.newLineSeparator()) return players().map { $0.pasteData(exportFormat) }.joined(separator: exportFormat.newLineSeparator())
case .csv: case .csv:
return players().map { [$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted() ].joined(separator: exportFormat.separator()) }.joined(separator: exportFormat.separator()) 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 +602,7 @@ final class TeamRegistration: ModelObject, Storable {
self.unsortedPlayers().sorted { (lhs, rhs) in self.unsortedPlayers().sorted { (lhs, rhs) in
let predicates: [AreInIncreasingOrder] = [ let predicates: [AreInIncreasingOrder] = [
{ $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 }, { $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.lastName < $1.lastName},
{ $0.firstName < $1.firstName } { $0.firstName < $1.firstName }
] ]
@ -642,6 +758,9 @@ final class TeamRegistration: ModelObject, Storable {
case _pointsEarned = "pointsEarned" case _pointsEarned = "pointsEarned"
case _unregistered = "unregistered" case _unregistered = "unregistered"
case _unregistrationDate = "unregistrationDate" case _unregistrationDate = "unregistrationDate"
case _clubCode = "clubCode"
case _clubName = "clubName"
case _registratonMail = "registratonMail"
} }
init(from decoder: Decoder) throws { init(from decoder: Decoder) throws {
@ -672,6 +791,10 @@ final class TeamRegistration: ModelObject, Storable {
finalRanking = try container.decodeIfPresent(Int.self, forKey: ._finalRanking) finalRanking = try container.decodeIfPresent(Int.self, forKey: ._finalRanking)
pointsEarned = try container.decodeIfPresent(Int.self, forKey: ._pointsEarned) pointsEarned = try container.decodeIfPresent(Int.self, forKey: ._pointsEarned)
unregistrationDate = try container.decodeIfPresent(Date.self, forKey: ._unregistrationDate) 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 { func encode(to encoder: Encoder) throws {
@ -700,6 +823,9 @@ final class TeamRegistration: ModelObject, Storable {
try container.encode(pointsEarned, forKey: ._pointsEarned) try container.encode(pointsEarned, forKey: ._pointsEarned)
try container.encode(unregistered, forKey: ._unregistered) try container.encode(unregistered, forKey: ._unregistered)
try container.encode(unregistrationDate, forKey: ._unregistrationDate) 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() { func insertOnServer() {

@ -430,6 +430,14 @@ final class Tournament : ModelObject, Storable {
// MARK: - Computed Dependencies // MARK: - Computed Dependencies
func unsortedTeams() -> [TeamRegistration] { func unsortedTeams() -> [TeamRegistration] {
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament unsortedTeams", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return Array(self.tournamentStore.teamRegistrations) return Array(self.tournamentStore.teamRegistrations)
} }
@ -597,9 +605,111 @@ defer {
teamPaste.append(team.pasteData(exportFormat, index + 1)) teamPaste.append(team.pasteData(exportFormat, index + 1))
} }
return teamPaste.joined(separator: exportFormat.newLineSeparator()) 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 - Vérif",
"JOUEUR 1 - Licence",
"JOUEUR 1 - Ranking",
"JOUEUR 1 - Statut",
"JOUEUR 2 - Nom",
"JOUEUR 2 - Prénom",
"JOUEUR 2 - Vérif",
"JOUEUR 2 - Licence",
"JOUEUR 2 - Ranking",
"JOUEUR 2 - Statut",
"JOUEUR 3 - Nom",
"JOUEUR 3 - Prénom",
"JOUEUR 3 - Vérif",
"JOUEUR 3 - Licence",
"JOUEUR 3 - Ranking",
"JOUEUR 3 - Statut",
"JOUEUR 4 - Nom",
"JOUEUR 4 - Prénom",
"JOUEUR 4 - Vérif",
"JOUEUR 4 - Licence",
"JOUEUR 4 - Ranking",
"JOUEUR 4 - Statut",
"JOUEUR 5 - Nom",
"JOUEUR 5 - Prénom",
"JOUEUR 5 - Vérif",
"JOUEUR 5 - Licence",
"JOUEUR 5 - Ranking",
"JOUEUR 5 - Statut",
"JOUEUR 6 - Nom",
"JOUEUR 6 - Prénom",
"JOUEUR 6 - Vérif",
"JOUEUR 6 - Licence",
"JOUEUR 6 - Ranking",
"JOUEUR 6 - Statut",
"JOUEUR 7 - Nom",
"JOUEUR 7 - Prénom",
"JOUEUR 7 - Vérif",
"JOUEUR 7 - Licence",
"JOUEUR 7 - Ranking",
"JOUEUR 7 - Statut",
"JOUEUR 8 - Nom",
"JOUEUR 8 - Prénom",
"JOUEUR 8 - Vérif",
"JOUEUR 8 - Licence",
"JOUEUR 8 - Ranking",
"JOUEUR 8 - Statut",
"JOUEUR 9 - Nom",
"JOUEUR 9 - Prénom",
"JOUEUR 9 - Vérif",
"JOUEUR 9 - Licence",
"JOUEUR 9 - Ranking",
"JOUEUR 9 - Statut",
"JOUEUR 10 - Nom",
"JOUEUR 10 - Prénom",
"JOUEUR 10 - Vérif",
"JOUEUR 10 - Licence",
"JOUEUR 10 - Ranking",
"JOUEUR 10 - Statut",
].joined(separator: exportFormat.separator())
var teamPaste = [headers]
let allLicenseIds = allLicenseIds()
for (index, team) in selectedSortedTeams.enumerated() {
print("pasting team index \(index)")
teamPaste.append(team.pasteData(exportFormat, index + 1, allPlayers: allLicenseIds))
}
return teamPaste.joined(separator: exportFormat.newLineSeparator())
} }
} }
func allLicenseIds() -> [(String, String)] {
players().compactMap({
if let id = $0.licenceId?.strippedLicense, let clubCode = $0.team()?.clubCode {
return (id, clubCode)
} else {
return nil
}
})
}
func club() -> Club? { func club() -> Club? {
return eventObject()?.clubObject() return eventObject()?.clubObject()
} }
@ -658,7 +768,7 @@ defer {
} }
func availableSeeds() -> [TeamRegistration] { func availableSeeds() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -895,7 +1005,7 @@ defer {
func selectedSortedTeams() -> [TeamRegistration] { func selectedSortedTeams() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -1002,11 +1112,28 @@ defer {
} }
func unsortedTeamsWithoutWO() -> [TeamRegistration] { func unsortedTeamsWithoutWO() -> [TeamRegistration] {
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func unsortedTeamsWithoutWO", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return self.tournamentStore.teamRegistrations.filter { $0.isOutOfTournament() == false } return self.tournamentStore.teamRegistrations.filter { $0.isOutOfTournament() == false }
// return Store.main.filter { $0.tournament == self.id && $0.walkOut == false } // return Store.main.filter { $0.tournament == self.id && $0.walkOut == false }
} }
func walkoutTeams() -> [TeamRegistration] { func walkoutTeams() -> [TeamRegistration] {
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func walkoutTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return self.tournamentStore.teamRegistrations.filter { $0.walkOut == true } return self.tournamentStore.teamRegistrations.filter { $0.walkOut == true }
// return Store.main.filter { $0.tournament == self.id && $0.walkOut == true } // return Store.main.filter { $0.tournament == self.id && $0.walkOut == true }
} }
@ -1027,6 +1154,14 @@ defer {
} }
func unsortedPlayers() -> [PlayerRegistration] { func unsortedPlayers() -> [PlayerRegistration] {
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func unsortedPlayers", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return Array(self.tournamentStore.playerRegistrations) return Array(self.tournamentStore.playerRegistrations)
} }
@ -1066,7 +1201,7 @@ defer {
//todo //todo
func significantPlayerCount() -> Int { func significantPlayerCount() -> Int {
return minimumPlayerPerTeam return 6
} }
func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] { func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
@ -1146,14 +1281,17 @@ defer {
previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory) previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory)
teamsToImport.append(previousTeam) teamsToImport.append(previousTeam)
} else { } else {
var registrationDate = team.registrationDate var registrationDate = team.registrationDate ?? team.teamChampionship?.getRegistrationDate()
if let previousPlayer = players.first(where: { player in if let previousPlayer = players.first(where: { player in
let ids = team.players.compactMap({ $0.licenceId }) let ids = team.players.compactMap({ $0.licenceId })
return ids.contains(player.licenceId!) return ids.contains(player.licenceId!)
}), let previousTeamRegistrationDate = previousPlayer.team()?.registrationDate { }), let previousTeamRegistrationDate = previousPlayer.team()?.registrationDate {
registrationDate = previousTeamRegistrationDate 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 isAnimation() {
if newTeam.weight == 0 { if newTeam.weight == 0 {
newTeam.weight = team.index(in: teams) ?? 0 newTeam.weight = team.index(in: teams) ?? 0
@ -1189,6 +1327,8 @@ defer {
} }
func registrationIssues() async -> Int { func registrationIssues() async -> Int {
return 0
let players : [PlayerRegistration] = unsortedPlayers() let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams() let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
@ -1226,7 +1366,7 @@ defer {
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament availableToStart", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(using: defaultSorting, order: .ascending) return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(using: defaultSorting, order: .ascending)
@ -1237,7 +1377,7 @@ defer {
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament runningMatches", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending) return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending)
@ -1248,7 +1388,7 @@ defer {
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament readyMatches", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
@ -1259,7 +1399,7 @@ defer {
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament readyMatches", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
@ -1267,11 +1407,11 @@ defer {
static func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] { static func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament finishedMatches", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
if let limit { if let limit {
@ -1480,13 +1620,19 @@ defer {
} }
} }
var championshipImportKey: String {
if federalAgeCategory == .senior {
return tournamentCategory.importingRawValue
} else {
return federalAgeCategory.exportingRawValue
}
}
func updateWeights() { func updateWeights() {
let teams = self.unsortedTeams() let teams = self.unsortedTeams()
teams.forEach { team in teams.forEach { team in
let players = team.unsortedPlayers() let players = team.unsortedPlayers()
players.forEach { $0.setComputedRank(in: self) } players.forEach { $0.setComputedRank(in: self) }
team.setWeight(from: players, inTournamentCategory: tournamentCategory) team.setWeight(from: players.sorted(by: \.computedRank), inTournamentCategory: tournamentCategory)
do { do {
try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
} catch { } catch {
@ -1501,6 +1647,15 @@ defer {
} }
func updateRank(to newDate: Date?) async throws { func updateRank(to newDate: Date?) async throws {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
guard let newDate else { return } guard let newDate else { return }
rankSourceDate = newDate rankSourceDate = newDate
@ -1524,11 +1679,32 @@ defer {
let lastRankWoman = currentMonthData()?.femaleUnrankedValue let lastRankWoman = currentMonthData()?.femaleUnrankedValue
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) }
let players = unsortedPlayers()
try await unsortedPlayers().concurrentForEach { player in try await players.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)
} }
} }
func verifyEQ() async throws {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
guard let newDate = URL.importDateFormatter.date(from: "08-2024") else { return }
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate }
let sources = dataURLs.map { CSVParser(url: $0) }
let teams = selectedSortedTeams().prefix(24)
let players = teams.flatMap({ $0.unsortedPlayers() })
try await players.concurrentForEach { player in
try await player.verifyEQ(from: sources)
}
}
func missingUnrankedValue() -> Bool { func missingUnrankedValue() -> Bool {
return maleUnrankedValue == nil || femaleUnrankedValue == nil return maleUnrankedValue == nil || femaleUnrankedValue == nil
@ -1592,7 +1768,7 @@ defer {
func availableQualifiedTeams() -> [TeamRegistration] { func availableQualifiedTeams() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -2027,7 +2203,7 @@ defer {
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration { func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: registrationDate, name: name) let team = TeamRegistration(tournament: id, registrationDate: registrationDate, name: name)
team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory) team.setWeight(from: players.sorted(by: \.computedRank), inTournamentCategory: tournamentCategory)
players.forEach { player in players.forEach { player in
player.teamRegistration = team.id player.teamRegistration = team.id
} }

@ -124,6 +124,15 @@ extension String {
return false return false
} }
func hasLicenseKey() -> Bool {
if let match = self.firstMatch(of: /[0-9]{6,8}[A-Z]/) {
return true
} else {
return false
}
}
var licenseKey: String? { var licenseKey: String? {
if let intValue = Int(self) { if let intValue = Int(self) {
var value = intValue var value = intValue
@ -165,6 +174,15 @@ extension String {
let matches = self.matches(of: /[1-9][0-9]{5,7}/) let matches = self.matches(of: /[1-9][0-9]{5,7}/)
return matches.map { String(self[$0.range]) } 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 // MARK: - FFT Source Importing
@ -200,11 +218,23 @@ extension LosslessStringConvertible {
extension String { extension String {
func createFile(_ withName: String = "temp", _ exportedFormat: ExportFormat = .rawText) -> URL { func createFile(_ withName: String = "temp", _ exportedFormat: ExportFormat = .rawText) -> URL {
let url = FileManager.default.temporaryDirectory let url = FileManager.default.temporaryDirectory
.appendingPathComponent(withName) .appendingPathComponent(withName)
.appendingPathExtension(exportedFormat.suffix) .appendingPathExtension(exportedFormat.suffix)
let string = self let string = self
try? FileManager.default.removeItem(at: url) try? FileManager.default.removeItem(at: url)
try? string.write(to: url, atomically: true, encoding: .utf8)
// Add BOM for Excel to properly recognize UTF-8
if exportedFormat == .championship || exportedFormat == .csv {
let bom = Data([0xEF, 0xBB, 0xBF])
let stringData = string.data(using: .utf8)!
let dataWithBOM = bom + stringData
try? dataWithBOM.write(to: url, options: .atomic)
} else {
// For other formats, write normally with UTF-8
try? string.write(to: url, atomically: true, encoding: .utf8)
}
return url return url
} }
} }

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

@ -131,13 +131,15 @@ class FileImportManager {
let previousTeam: TeamRegistration? let previousTeam: TeamRegistration?
var registrationDate: Date? = nil var registrationDate: Date? = nil
var name: String? = 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.players = Set(players)
self.tournamentCategory = tournamentCategory self.tournamentCategory = tournamentCategory
self.tournamentAgeCategory = tournamentAgeCategory self.tournamentAgeCategory = tournamentAgeCategory
self.name = name self.name = name
self.previousTeam = previousTeam self.previousTeam = previousTeam
self.teamChampionship = teamChampionship
if players.count < 2 { if players.count < 2 {
let s = players.compactMap { $0.sex?.rawValue } let s = players.compactMap { $0.sex?.rawValue }
var missing = tournamentCategory.mandatoryPlayerType() var missing = tournamentCategory.mandatoryPlayerType()
@ -146,7 +148,7 @@ class FileImportManager {
missing.remove(at: index) missing.remove(at: index)
} }
} }
let significantPlayerCount = 2 let significantPlayerCount = tournament.significantPlayerCount()
let pl = players.prefix(significantPlayerCount).map { $0.computedRank } 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) 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,+) 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" 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 { switch fileProvider {
case .frenchFederation: case .frenchFederation:
@ -187,9 +189,9 @@ class FileImportManager {
case .padelClub: case .padelClub:
return await _getPadelClubTeams(from: fileContent, tournament: tournament) return await _getPadelClubTeams(from: fileContent, tournament: tournament)
case .custom: 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: 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 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") let lines = fileContent.replacingOccurrences(of: "\"", with: "").components(separatedBy: "\n")
guard let firstLine = lines.first else { return [] } guard let firstLine = lines.first else { return [] }
var separator = "," var separator = ","
@ -436,13 +438,27 @@ class FileImportManager {
let federalContext = PersistenceController.shared.localContainer.viewContext let federalContext = PersistenceController.shared.localContainer.viewContext
var chunks: [[String]] = [] var chunks: [[String]] = []
if chunkByParameter { switch chunkMode {
case .byParameter:
chunks = lines.chunked(byParameterAt: 1) chunks = lines.chunked(byParameterAt: 1)
} else { case .byCoupleOfLines:
chunks = lines.chunked(into: 2) chunks = lines.chunked(into: 2)
case .byColumn:
chunks = lines.extractPlayers(filterKey: tournament.championshipImportKey.capitalized, separator: separator)
} }
let results = chunks.map { team in
let results = chunks.map { teamSource in
var teamName: String? = nil 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 players = team.map { player in
let data = player.components(separatedBy: separator) let data = player.components(separatedBy: separator)
let lastName : String = data[safe: 2]?.prefixTrimmed(50) ?? "" let lastName : String = data[safe: 2]?.prefixTrimmed(50) ?? ""
@ -456,27 +472,84 @@ class FileImportManager {
let rank : Int? = data[safe: 6]?.trimmed.toInt() let rank : Int? = data[safe: 6]?.trimmed.toInt()
let licenceId : String? = data[safe: 7]?.prefixTrimmed(50) let licenceId : String? = data[safe: 7]?.prefixTrimmed(50)
let club : String? = data[safe: 8]?.prefixTrimmed(200) let club : String? = data[safe: 8]?.prefixTrimmed(200)
let predicate = NSPredicate(format: "firstName like[cd] %@ && lastName like[cd] %@", firstName, lastName) let status : String? = data[safe: 9]
fetchRequest.predicate = predicate let verified : String? = data[safe: 10]
let found = try? federalContext.fetch(fetchRequest).first let isVerified = verified == "ok"
if let found, autoSearch { let isEQVerified = verified == "ok2"
let player = PlayerRegistration(importedPlayer: found) if chunkMode == .byColumn {
player.setComputedRank(in: tournament)
player.email = email if let licenceId = licenceId?.strippedLicense {
player.phoneNumber = phoneNumber let predicate = NSPredicate(format: "license == %@", licenceId)
return player 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
if isEQVerified {
player.source = .frenchFederationEQVerified
} else if isVerified {
player.source = .frenchFederationVerified
}
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 isEQVerified {
player.source = .frenchFederationEQVerified
} else if isVerified {
player.source = .frenchFederationVerified
}
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)
player.sourceName = lastName
player.isNveq = status == "NVEQ"
if isEQVerified {
player.source = .frenchFederationEQVerified
} else if isVerified {
player.source = .frenchFederationVerified
}
if rank == nil {
player.setComputedRank(in: tournament)
} else {
player.computedRank = rank ?? 0
}
return player
}
} else { } else {
let player = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: licenceId, rank: rank, sex: sex, clubName: club, phoneNumber: phoneNumber, email: email) let predicate = NSPredicate(format: "firstName like[cd] %@ && lastName like[cd] %@", firstName, lastName)
if rank == nil, autoSearch { fetchRequest.predicate = predicate
let found = try? federalContext.fetch(fetchRequest).first
if let found, autoSearch {
let player = PlayerRegistration(importedPlayer: found)
player.setComputedRank(in: tournament) player.setComputedRank(in: tournament)
player.email = email
player.phoneNumber = phoneNumber
return player
} else { } 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 return results
} }
@ -513,5 +586,292 @@ extension Array where Element == String {
return groups.map { $0.value } 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
let count = 6
for i in 0..<10 {
let lastNameIndex = 12 + (i * count)
let firstNameIndex = 13 + (i * count)
let validationStatusIndex = 14 + (i * count)
let licenseIndex = 15 + (i * count)
let rankingIndex = 16 + (i * count)
let statusIndex = 17 + (i * count)
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,
validationStatus: components[validationStatusIndex]
)
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?
let validationStatus: 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],
validationStatus: nil
)
}
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],
validationStatus: nil
)
}
func rawValue(separator: String = ";", sex: String = "m") -> String {
let components = [
sex,
"",
lastName.trimmed,
firstName.trimmed,
"",
"",
"",
licenseNumber.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines),
"",
status.rawValue,
validationStatus ?? ""
]
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)
case duplicate(Int, PlayerRegistration)
case notEQ(Bool?, PlayerRegistration)
case isYearValid(Bool?, PlayerRegistration)
var errorDescription: String? {
switch self {
case .isYearValid(_, let playerRegistration):
return playerRegistration.errorDescription(championshipAlert: self)
case .notEQ(_, let playerRegistration):
return playerRegistration.errorDescription(championshipAlert: self)
case .duplicate(_, let playerRegistration):
return playerRegistration.errorDescription(championshipAlert: self)
case .clubCodeInvalid(let teamRegistration):
if let clubCode = teamRegistration.clubCode {
return "CODE NOK : \(clubCode)"
} else {
return "aucun code club"
}
case .tooManyNVEQ:
return "TOO MANY NVEQ"
case .tooManyPlayers:
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 = self.playerLabel() + " - " + self.formattedLicense() + " -> "
switch championshipAlert {
case .isYearValid(let bool, _):
if bool == nil {
message += "MILLESIME INVERIFIABLE"
} else {
message += "MILLESIME INVALIDE"
}
case .notEQ(let bool, _):
if bool == nil {
message += "EQ INVERIFIABLE"
} else {
message += "EQ INVALIDE"
}
case .duplicate(let count, _):
message += "DOUBLON : " + count.formatted()
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 licenceId != nil {
message += "LICENSE MISSTYPE"
} 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
}
}

@ -194,6 +194,27 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable {
} }
} }
var exportingRawValue: String {
switch self {
case .unlisted:
return "Animation"
case .a11_12:
return "U2 ans"
case .a13_14:
return "U14 ans"
case .a15_16:
return "U16 ans"
case .a17_18:
return "U18 ans"
case .senior:
return "Senior"
case .a45:
return "45 ans"
case .a55:
return "55 ans"
}
}
var importingRawValue: String { var importingRawValue: String {
switch self { switch self {
case .unlisted: case .unlisted:

@ -65,6 +65,10 @@ struct Line: Identifiable {
var assimilation: String? { var assimilation: String? {
data[7] data[7]
} }
var clubCode: String? {
data[10]?.replaceCharactersFromSet(characterSet: .whitespaces)
}
} }
struct CSVParser: AsyncSequence, AsyncIteratorProtocol { struct CSVParser: AsyncSequence, AsyncIteratorProtocol {

@ -18,7 +18,7 @@ struct TournamentLookUpView: View {
@State private var searchField: String = "" @State private var searchField: String = ""
@State var page: Int = 0 @State var page: Int = 0
@State var total: Int = 0 @State var total: Int = 0
@State private var showingSettingsAlert = false
@State private var searching: Bool = false @State private var searching: Bool = false
@State private var requestedToGetAllPages: Bool = false @State private var requestedToGetAllPages: Bool = false
@State private var revealSearchParameters: Bool = true @State private var revealSearchParameters: Bool = true
@ -57,6 +57,16 @@ struct TournamentLookUpView: View {
} message: { } message: {
Text("Aucune ville n'a été indiqué, il est préférable de se localiser ou d'indiquer une ville pour réduire le nombre de résultat.") Text("Aucune ville n'a été indiqué, il est préférable de se localiser ou d'indiquer une ville pour réduire le nombre de résultat.")
} }
.alert(isPresented: $showingSettingsAlert) {
Alert(
title: Text("Réglages"),
message: Text("Pour trouver les clubs autour de vous, vous devez l'autorisation à Padel Club de récupérer votre position."),
primaryButton: .default(Text("Ouvrir les réglages"), action: {
_openSettings()
}),
secondaryButton: .cancel()
)
}
.alert("Attention", isPresented: $presentAlert, actions: { .alert("Attention", isPresented: $presentAlert, actions: {
Button { Button {
presentAlert = false presentAlert = false
@ -305,9 +315,15 @@ struct TournamentLookUpView: View {
} }
if locationManager.requestStarted { if locationManager.requestStarted {
ProgressView() ProgressView()
} else { } else if locationManager.manager.authorizationStatus != .restricted {
LocationButton { LocationButton {
locationManager.requestLocation() if locationManager.manager.authorizationStatus == .notDetermined {
locationManager.manager.requestWhenInUseAuthorization()
} else if locationManager.manager.authorizationStatus == .denied {
showingSettingsAlert = true
} else {
locationManager.requestLocation()
}
} }
.symbolVariant(.fill) .symbolVariant(.fill)
.foregroundColor (Color.white) .foregroundColor (Color.white)
@ -485,4 +501,12 @@ struct TournamentLookUpView: View {
return "Distance" return "Distance"
} }
} }
private func _openSettings() {
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else {
return
}
UIApplication.shared.open(settingsURL)
}
} }

@ -176,6 +176,7 @@ struct MainView: View {
} }
private func _checkSourceFileAvailability() async { private func _checkSourceFileAvailability() async {
return
print("dataStore.appSettings.lastDataSource :", dataStore.appSettings.lastDataSource ?? "none") print("dataStore.appSettings.lastDataSource :", dataStore.appSettings.lastDataSource ?? "none")
print("check internet") print("check internet")
print("check files on internet") print("check files on internet")

@ -250,7 +250,8 @@ struct PadelClubView: View {
// Function to fetch data for a single license ID // Function to fetch data for a single license ID
func fetchPlayerData(for licenseID: String) async throws -> [Player]? { 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) 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("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("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("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") request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
// Add cookies if needed (example cookie header value shown, replace with valid cookies) // 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=D4F6672C85C69F2221A34DA7573E6DB2; AWSALB=r+eQo2F3BgjLCg2GN+hICmxbd2iUrF9Q6Eq7ihPvtv7Hjpj7PDfvXIl2s6Ny++tjlbZRE4B48u8NiV+LfmYuuDCiCi5M3HkYn9a9/SUBK7TbH1UUyGmH+3Wsr67R; AWSALBCORS=r+eQo2F3BgjLCg2GN+hICmxbd2iUrF9Q6Eq7ihPvtv7Hjpj7PDfvXIl2s6Ny++tjlbZRE4B48u8NiV+LfmYuuDCiCi5M3HkYn9a9/SUBK7TbH1UUyGmH+3Wsr67R; datadome=QNA97FsdNhy8W9ry4jSSpxMF_HlDz56VkztCkM2u3GIropzVSIljKI9jT_Nve4g8OgtCT_MTxZRi8Xpwe3xJ6d5Lygt7S7Eos8dQqddCeWYaeJDgTreRIDrNYfIoZMwI; xtan=-; xtant=1; tc_cj_v2=%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQMMORPMMMQNNZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQMMQMPMPMKOPZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQMPLNJNKJQQJZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQMPLNJNLQOJKZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQMPRQLQPMPLRZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; SSESS7ba44afc36c80c3faa2b8fa87e7742c5=Wfc_Bx21iDLRWgzfE6duKBHsSo7FvYqlqz7J_JmIgXc; _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 (data, _) = try await URLSession.shared.data(for: request)
let decoder = JSONDecoder() let decoder = JSONDecoder()
@ -293,6 +294,8 @@ func fetchPlayersDataSequentially(for licenseIDs: inout [FederalPlayer]) async {
if let playerData = try await fetchPlayerData(for: licenseID.license)?.first { if let playerData = try await fetchPlayerData(for: licenseID.license)?.first {
licenseID.lastName = playerData.nom licenseID.lastName = playerData.nom
licenseID.firstName = playerData.prenom licenseID.firstName = playerData.prenom
licenseID.birthYear = playerData.birthYear()
licenseID.clubCode = playerData.codeClub
} }
} catch { } catch {
print(error) print(error)
@ -306,6 +309,14 @@ struct Player: Codable {
let nom: String let nom: String
let prenom: String let prenom: String
let sexe: String let sexe: String
let codeClub: String
let dateNaissanceFr: String
let millesimeLicence: Int
let rang: Int?
func birthYear() -> Int? {
return Int(dateNaissanceFr.suffix(4))
}
} }
struct Response: Codable { struct Response: Codable {

@ -15,7 +15,7 @@ struct LoserRoundView: View {
let loserBracket: LoserRound let loserBracket: LoserRound
private func _roundDisabled() -> Bool { private func _roundDisabled() -> Bool {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)

@ -42,7 +42,7 @@ extension UpperRound: Equatable {
} }
func badgeValue() -> Int? { func badgeValue() -> Int? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -57,7 +57,7 @@ extension UpperRound: Equatable {
} }
func badgeImage() -> Badge? { func badgeImage() -> Badge? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -85,7 +85,7 @@ struct LoserRound: Identifiable, Selectable {
} }
var shouldBeDisplayed: Bool { var shouldBeDisplayed: Bool {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -97,7 +97,7 @@ struct LoserRound: Identifiable, Selectable {
} }
static func updateDestinations(fromLoserRounds loserRounds: [Round], inUpperBracketRound upperBracketRound: Round) -> [LoserRound] { static func updateDestinations(fromLoserRounds loserRounds: [Round], inUpperBracketRound upperBracketRound: Round) -> [LoserRound] {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -146,7 +146,7 @@ extension LoserRound: Equatable {
} }
func badgeImage() -> Badge? { func badgeImage() -> Badge? {
#if _DEBUG_TIME //DEBUGING TIME #if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)

@ -11,9 +11,10 @@ struct TeamWeightView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
let team: TeamRegistration let team: TeamRegistration
var teamPosition: TeamPosition? = nil var teamPosition: TeamPosition? = nil
var teamIndex: Int?
var teamIndex: Int? { private var _teamIndex: Int? {
team.tournamentObject()?.indexOf(team: team) teamIndex ?? team.tournamentObject()?.indexOf(team: team)
} }
var displayWeight: Bool { var displayWeight: Bool {
@ -27,8 +28,8 @@ struct TeamWeightView: View {
.monospacedDigit() .monospacedDigit()
.font(.caption) .font(.caption)
} }
if let teamIndex { if let _teamIndex {
Text("#" + (teamIndex + 1).formatted(.number.precision(.integerLength(2...3)))) Text("#" + (_teamIndex + 1).formatted(.number.precision(.integerLength(2...3))))
.monospacedDigit() .monospacedDigit()
} }
if teamPosition == .two && displayWeight { if teamPosition == .two && displayWeight {

@ -10,13 +10,14 @@ import SwiftUI
struct TeamRowView: View { struct TeamRowView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
var team: TeamRegistration var team: TeamRegistration
var teamIndex: Int?
var teamPosition: TeamPosition? = nil var teamPosition: TeamPosition? = nil
var displayCallDate: Bool = false var displayCallDate: Bool = false
var displayRestingTime: Bool = false var displayRestingTime: Bool = false
var body: some View { var body: some View {
LabeledContent { LabeledContent {
TeamWeightView(team: team, teamPosition: teamPosition) TeamWeightView(team: team, teamPosition: teamPosition, teamIndex: teamIndex)
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
TeamHeadlineView(team: team) TeamHeadlineView(team: team)

@ -86,21 +86,6 @@ struct FileImportView: View {
@State private var validatedTournamentIds: Set<String> = Set() @State private var validatedTournamentIds: Set<String> = Set()
@State private var chunkMode: ChunkMode = .byParameter @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) { init(defaultFileProvider: FileImportManager.FileProvider = .frenchFederation) {
_fileProvider = .init(wrappedValue: defaultFileProvider) _fileProvider = .init(wrappedValue: defaultFileProvider)
} }
@ -109,10 +94,6 @@ struct FileImportView: View {
return self.tournament.tournamentStore return self.tournament.tournamentStore
} }
var chunkByParameter: Bool {
return chunkMode == .byParameter
}
private func filteredTeams(tournament: Tournament) -> [FileImportManager.TeamHolder] { private func filteredTeams(tournament: Tournament) -> [FileImportManager.TeamHolder] {
if tournament.isAnimation() { if tournament.isAnimation() {
return teams.sorted(by: \.weight) return teams.sorted(by: \.weight)
@ -547,7 +528,7 @@ struct FileImportView: View {
for someTournament in event.tournaments { for someTournament in event.tournaments {
let combinedCategory = CombinedCategory(tournamentCategory: someTournament.tournamentCategory, federalTournamentAge: someTournament.federalTournamentAge) let combinedCategory = CombinedCategory(tournamentCategory: someTournament.tournamentCategory, federalTournamentAge: someTournament.federalTournamentAge)
if categoriesDone.contains(combinedCategory) == false { 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 self.teams += _teams
categoriesDone.append(combinedCategory) categoriesDone.append(combinedCategory)
} else { } else {
@ -555,7 +536,7 @@ struct FileImportView: View {
} }
} }
} else { } 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 { await MainActor.run {

@ -43,30 +43,29 @@ struct UpdateSourceRankDateView: View {
Task { Task {
do { do {
try await tournament.updateRank(to: currentRankSourceDate) try await tournament.updateRank(to: currentRankSourceDate)
try await MainActor.run { let unsortedPlayers = tournament.unsortedPlayers()
tournament.unsortedPlayers().forEach { player in tournament.unsortedPlayers().forEach { player in
player.setComputedRank(in: tournament) player.setComputedRank(in: tournament)
} }
try tournamentStore.playerRegistrations.addOrUpdate(contentOfs: tournament.unsortedPlayers()) try tournamentStore.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers)
tournament.unsortedTeams().forEach { team in let unsortedTeams = tournament.unsortedTeams()
team.setWeight(from: team.players(), inTournamentCategory: tournament.tournamentCategory) unsortedTeams.forEach { team in
if forceRefreshLockWeight { team.setWeight(from: team.players(), inTournamentCategory: tournament.tournamentCategory)
team.lockedWeight = team.weight if forceRefreshLockWeight {
} team.lockedWeight = team.weight
} }
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
try dataStore.tournaments.addOrUpdate(instance: tournament)
updatingRank = false
confirmUpdateRank = false
} }
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
updatingRank = false
confirmUpdateRank = false
} }
}.disabled(updatingRank) }.disabled(updatingRank)

@ -52,6 +52,13 @@ struct InscriptionManagerView: View {
@State private var refreshResult: String? = nil @State private var refreshResult: String? = nil
@State private var refreshInProgress: Bool = false @State private var refreshInProgress: Bool = false
@State private var refreshStatus: Bool? @State private var refreshStatus: Bool?
@State private var gatheringInProgress: Bool = false
@State private var gathered: Double = 0
@State private var gatheringDone: Bool = false
@State private var totalUnrankedUnsourced: Double = 0
@State private var selectedSortedTeams: [TeamRegistration] = []
@State private var shareFile: URL?
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return self.tournament.tournamentStore return self.tournament.tournamentStore
@ -186,14 +193,18 @@ struct InscriptionManagerView: View {
} }
private func _setHash() { private func _setHash() {
#if _DEBUG_TIME //DEBUGING TIME self.selectedSortedTeams = self.tournament.selectedSortedTeams()
return
#if DEBUG //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
let selectedSortedTeams = tournament.selectedSortedTeams() //let selectedSortedTeams = tournament.selectedSortedTeams()
if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { if self.teamsHash == nil, selectedSortedTeams.isEmpty == false {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
} }
@ -204,7 +215,7 @@ struct InscriptionManagerView: View {
} }
private func _handleHashDiff() { private func _handleHashDiff() {
let selectedSortedTeams = tournament.selectedSortedTeams() //let selectedSortedTeams = tournament.selectedSortedTeams()
let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) { if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) {
self.teamsHash = newHash self.teamsHash = newHash
@ -461,15 +472,19 @@ struct InscriptionManagerView: View {
private func _sharingTeamsMenuView() -> some View { private func _sharingTeamsMenuView() -> some View {
Menu { Menu {
if let teamPaste = teamPaste() { // if let teamPaste = teamPaste() {
ShareLink(item: teamPaste) { // ShareLink(item: teamPaste) {
Text("En texte") // Text("En texte")
} // }
} // }
if let teamPaste = teamPaste(.csv) { // if let teamPaste = teamPaste(.csv) {
ShareLink(item: teamPaste) { // ShareLink(item: teamPaste) {
Text("En csv") // Text("En csv")
} // }
// }
ShareLink(item: teamPaste(.championship), preview: .init("championship")) {
Text("championship")
} }
} label: { } label: {
Label("Exporter les paires", systemImage: "square.and.arrow.up") Label("Exporter les paires", systemImage: "square.and.arrow.up")
@ -484,8 +499,8 @@ struct InscriptionManagerView: View {
tournament.unsortedTeamsWithoutWO() tournament.unsortedTeamsWithoutWO()
} }
func teamPaste(_ exportFormat: ExportFormat = .rawText) -> URL? { func teamPaste(_ exportFormat: ExportFormat = .rawText) -> TournamentShareFile {
tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle(.short), exportFormat) TournamentShareFile(tournament: tournament, exportFormat: exportFormat)
} }
var unsortedPlayers: [PlayerRegistration] { var unsortedPlayers: [PlayerRegistration] {
@ -494,9 +509,9 @@ struct InscriptionManagerView: View {
var sortedTeams: [TeamRegistration] { var sortedTeams: [TeamRegistration] {
if filterMode == .waiting { if filterMode == .waiting {
return tournament.waitingListSortedTeams() return tournament.waitingListTeams(in: selectedSortedTeams, includingWalkOuts: false)
} else { } else {
return tournament.sortedTeams() return selectedSortedTeams + tournament.waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true)
} }
} }
@ -575,7 +590,7 @@ struct InscriptionManagerView: View {
private func _teamRegisteredView() -> some View { private func _teamRegisteredView() -> some View {
List { List {
let selectedSortedTeams = tournament.selectedSortedTeams() let sortedTeams = sortedTeams
if presentSearch == false { if presentSearch == false {
_informationView() _informationView()
@ -619,7 +634,7 @@ struct InscriptionManagerView: View {
EditingTeamView(team: team) EditingTeamView(team: team)
.environment(tournament) .environment(tournament)
} label: { } label: {
TeamRowView(team: team) TeamRowView(team: team, teamIndex: teamIndex)
} }
.swipeActions(edge: .trailing, allowsFullSwipe: true) { .swipeActions(edge: .trailing, allowsFullSwipe: true) {
_teamDeleteButtonView(team) _teamDeleteButtonView(team)
@ -725,15 +740,15 @@ struct InscriptionManagerView: View {
private func _teamCountForFilterMode(filterMode: FilterMode) -> String { private func _teamCountForFilterMode(filterMode: FilterMode) -> String {
switch filterMode { switch filterMode {
case .wildcardBracket: case .wildcardBracket:
return tournament.selectedSortedTeams().filter({ $0.wildCardBracket }).count.formatted() return selectedSortedTeams.filter({ $0.wildCardBracket }).count.formatted()
case .wildcardGroupStage: case .wildcardGroupStage:
return tournament.selectedSortedTeams().filter({ $0.wildCardGroupStage }).count.formatted() return selectedSortedTeams.filter({ $0.wildCardGroupStage }).count.formatted()
case .all: case .all:
return unsortedTeamsWithoutWO.count.formatted() return unsortedTeamsWithoutWO.count.formatted()
case .bracket: case .bracket:
return tournament.selectedSortedTeams().filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted() return selectedSortedTeams.filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted()
case .groupStage: case .groupStage:
return tournament.selectedSortedTeams().filter({ $0.inGroupStage() }).count.formatted() return selectedSortedTeams.filter({ $0.inGroupStage() }).count.formatted()
case .walkOut: case .walkOut:
let wo = walkoutTeams.count.formatted() let wo = walkoutTeams.count.formatted()
return wo return wo
@ -858,6 +873,161 @@ struct InscriptionManagerView: View {
} }
} }
} }
Button {
Task {
DispatchQueue.main.async {
gatheringInProgress = true
gathered = 0
gatheringDone = false
}
let unrankedUnsourced = tournament.players().filter { $0.isUnranked() && $0.source == nil }
DispatchQueue.main.async {
totalUnrankedUnsourced = Double(unrankedUnsourced.count)
print("total", unrankedUnsourced.count)
}
await withTaskGroup(of: Void.self) { group in
var playersToSave = [PlayerRegistration]()
for player in unrankedUnsourced {
group.addTask {
do {
if let playerData = try await player.fetchUnrankPlayerData() {
player.lastName = playerData.nom
player.birthdate = playerData.dateNaissanceFr
player.clubCode = playerData.codeClub
player.isYearValid = playerData.millesimeLicence == 2025
player.source = .frenchFederation
player.rank = playerData.rang
playersToSave.append(player)
}
DispatchQueue.main.async {
gathered += 1
print("gathered", gathered)
}
} catch {
print("Error fetching data for player \(player):", error)
}
}
do {
try tournamentStore.playerRegistrations.addOrUpdate(contentOfs: playersToSave)
} catch {
Logger.error(error)
}
}
// Wait for all tasks to complete
await group.waitForAll()
}
gatheringDone = true
gatheringInProgress = false
}
} label: {
if gatheringInProgress {
LabeledContent {
Text("\(gathered.formatted()) / \(totalUnrankedUnsourced.formatted())")
} label: {
ProgressView("Récupérés", value: gathered, total: totalUnrankedUnsourced)
}
} else {
LabeledContent {
if gatheringDone {
Image(systemName: "checkmark").foregroundStyle(.green)
}
} label: {
Text("Récupérer les non-classés")
}
}
}
Button {
Task {
DispatchQueue.main.async {
gatheringInProgress = true
gathered = 0
gatheringDone = false
}
let unrankedUnsourcedChunk = tournament.players().filter { $0.isYearValid == nil }.chunked(into: 100)
for unrankedUnsourced in unrankedUnsourcedChunk {
await withTaskGroup(of: Void.self) { group in
var playersToSave = [PlayerRegistration]()
for player in unrankedUnsourced {
group.addTask {
do {
if let playerData = try await player.fetchUnrankPlayerData() {
player.lastName = playerData.nom
player.birthdate = playerData.dateNaissanceFr
player.clubCode = playerData.codeClub
player.isYearValid = playerData.millesimeLicence == 2025
//player.source = .frenchFederation
playersToSave.append(player)
}
DispatchQueue.main.async {
gathered += 1
print("gathered", gathered)
}
} catch {
print("Error fetching data for player \(player):", error)
}
}
do {
try tournamentStore.playerRegistrations.addOrUpdate(contentOfs: playersToSave)
} catch {
Logger.error(error)
}
}
// Wait for all tasks to complete
await group.waitForAll()
}
}
gatheringDone = true
gatheringInProgress = false
}
} label: {
if gatheringInProgress {
LabeledContent {
Text("\(gathered.formatted()) / \(totalUnrankedUnsourced.formatted())")
} label: {
ProgressView("Récupérés", value: gathered, total: totalUnrankedUnsourced)
}
} else {
LabeledContent {
if gatheringDone {
Image(systemName: "checkmark").foregroundStyle(.green)
}
} label: {
Text("Check millesime")
}
}
}
Button("Recalculer les poids") {
tournament.updateWeights()
_setHash()
}
RowButtonView("Verify EQ") {
await try? tournament.verifyEQ()
}
Button("Generate file") {
self.shareFile = tournament.pasteDataForImporting(.championship).createFile(self.tournament.tournamentTitle()+"-inscriptions", .championship)
}
if let shareFile {
ShareLink(item: shareFile)
}
} header: { } header: {
HStack { HStack {
Spacer() Spacer()
@ -1124,3 +1294,20 @@ struct InscriptionManagerView: View {
// .environment(Tournament.mock()) // .environment(Tournament.mock())
// } // }
//} //}
struct TournamentShareFile: Transferable {
let tournament: Tournament
let exportFormat: ExportFormat
func shareFile() -> URL {
print("Generating URL...")
return tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle()+"-inscriptions", exportFormat)
}
static var transferRepresentation: some TransferRepresentation {
ProxyRepresentation { transferable in
return transferable.shareFile()
}
}
}

Loading…
Cancel
Save