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. 180
      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. 31
      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_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3291,7 +3291,10 @@
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_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_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3302,8 +3305,8 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.34;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
MARKETING_VERSION = 1.0.42;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.dec;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -3323,7 +3326,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3335,7 +3338,10 @@
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_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_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3346,8 +3352,8 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.34;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
MARKETING_VERSION = 1.0.42;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.dec;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -3450,8 +3456,12 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
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_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_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3493,8 +3503,12 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
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_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_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3537,6 +3551,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
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_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3578,6 +3593,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
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_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
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] {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -312,7 +312,7 @@ final class GroupStage: ModelObject, Storable {
}
func runningMatches(playedMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -323,7 +323,7 @@ final class GroupStage: ModelObject, Storable {
}
func readyMatches(playedMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -334,7 +334,7 @@ final class GroupStage: ModelObject, Storable {
}
func finishedMatches(playedMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
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 {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -132,7 +132,7 @@ defer {
}
func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -808,7 +808,7 @@ defer {
}
func teams() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -883,7 +883,7 @@ defer {
}
func team(_ team: TeamPosition) -> TeamRegistration? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)

@ -41,9 +41,16 @@ final class PlayerRegistration: ModelObject, Storable {
var coach: 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 {
switch source {
case .frenchFederation:
case .frenchFederation, .frenchFederationVerified, .frenchFederationEQVerified:
return "Via la base fédérale"
case .beachPadel:
return "Via le fichier beach-padel"
@ -159,13 +166,120 @@ final class PlayerRegistration: ModelObject, Storable {
return nil
}
func fetchUnrankPlayerData() async throws -> Player? {
guard let licence = licenceId?.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).trimmed.strippedLicense else {
return nil
}
return try await fetchPlayerData(for: licence)?.first
}
func pasteData(_ exportFormat: ExportFormat = .rawText) -> String {
switch exportFormat {
case .rawText:
return [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: exportFormat.separator())
case .csv:
return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator())
case .championship:
let values = [
lastName.uppercased(),
firstName.capitalized,
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 {
@ -257,8 +371,33 @@ final class PlayerRegistration: ModelObject, Storable {
}
}
@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 {
#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) {
rank = dataFound.rankValue?.toInt()
points = dataFound.points
@ -269,6 +408,14 @@ final class PlayerRegistration: ModelObject, Storable {
}
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 {
return try await historyFromName(from: sources)
}
@ -294,6 +441,14 @@ final class PlayerRegistration: ModelObject, Storable {
}
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
for source in sources.filter({ $0.maleData == isMalePlayer() }) {
group.addTask { [lastName, firstName] in
@ -380,6 +535,12 @@ final class PlayerRegistration: ModelObject, Storable {
case _hasArrived = "hasArrived"
case _coach = "coach"
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)
birthdate = try container.decodeIfPresent(String.self, forKey: ._birthdate)
source = try container.decodeIfPresent(PlayerDataSource.self, forKey: ._source)
clubCode = try container.decodeIfPresent(String.self, forKey: ._clubCode)
isNveq = try container.decodeIfPresent(Bool.self, forKey: ._isNveq) ?? false
sourceName = try container.decodeIfPresent(String.self, forKey: ._sourceName)
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 {
@ -437,12 +605,20 @@ final class PlayerRegistration: ModelObject, Storable {
try container.encode(hasArrived, forKey: ._hasArrived)
try container.encode(captain, forKey: ._captain)
try container.encode(coach, forKey: ._coach)
try container.encode(clubCode, forKey: ._clubCode)
try container.encode(sourceName, forKey: ._sourceName)
try container.encode(isNveq, forKey: ._isNveq)
try container.encode(isEQConfirmed, forKey: ._isEQConfirmed)
try container.encode(duplicatePlayers, forKey: ._duplicatePlayers)
try container.encode(isYearValid, forKey: ._isYearValid)
}
enum PlayerDataSource: Int, Codable {
case frenchFederation = 0
case beachPadel = 1
case onlineRegistration = 2
case frenchFederationVerified = 3
case frenchFederationEQVerified = 4
}
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? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -217,7 +217,7 @@ defer {
}
func upperBracketTopMatch(ofMatchIndex matchIndex: Int, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -237,7 +237,7 @@ defer {
}
func upperBracketBottomMatch(ofMatchIndex matchIndex: Int, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -259,7 +259,7 @@ defer {
func topPreviousRoundMatch(ofMatch match: Match, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -275,7 +275,7 @@ defer {
}
func bottomPreviousRoundMatch(ofMatch match: Match, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -302,7 +302,7 @@ defer {
}
// func displayableMatches() -> [Match] {
//#if _DEBUG_TIME //DEBUGING TIME
//#if DEBUG //DEBUGING TIME
// let start = Date()
// defer {
// let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -328,7 +328,7 @@ defer {
}
func previousRound() -> Round? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -462,7 +462,7 @@ defer {
func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let _cachedSeedInterval { return _cachedSeedInterval.localizedLabel(displayStyle) }
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -513,7 +513,7 @@ defer {
func seedInterval(initialMode: Bool = false) -> SeedInterval? {
if initialMode == false, let _cachedSeedInterval { return _cachedSeedInterval }
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -607,7 +607,7 @@ defer {
func loserRounds() -> [Round] {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -801,7 +801,7 @@ extension Round: Selectable, Equatable {
}
func badgeValue() -> Int? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -822,7 +822,7 @@ extension Round: Selectable, Equatable {
}
func badgeImage() -> Badge? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)

@ -42,6 +42,10 @@ final class TeamRegistration: ModelObject, Storable {
var unregistered: Bool = false
var unregistrationDate: Date? = nil
var clubCode: String?
var registratonMail: String?
var clubName: String?
func hasUnregistered() -> Bool {
unregistered
}
@ -86,7 +90,7 @@ final class TeamRegistration: ModelObject, Storable {
// MARK: - Computed dependencies
func unsortedPlayers() -> [PlayerRegistration] {
return self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id && $0.coach == false }
return self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id && $0.coach == false && $0.captain == false }
}
// MARK: -
@ -375,15 +379,116 @@ final class TeamRegistration: ModelObject, Storable {
resetBracketPosition()
}
func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String {
func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0, allPlayers: [(String, String)] = []) -> String {
switch exportFormat {
case .rawText:
return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name].compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
case .csv:
return [index.formatted(), playersPasteData(exportFormat), isWildCard() ? "WC" : weight.formatted()].joined(separator: exportFormat.separator())
case .championship:
var baseValue: [String] = [
formattedInscriptionDate(exportFormat) ?? "",
alertCountFormatted(teamIndex: index, 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 {
return registrationDate ?? .distantFuture
}
@ -402,6 +507,15 @@ final class TeamRegistration: ModelObject, Storable {
} else {
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 {
return nil
}
case .csv:
case .csv, .championship:
if let callDate {
return callDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else {
@ -429,6 +543,8 @@ final class TeamRegistration: ModelObject, Storable {
return players().map { $0.pasteData(exportFormat) }.joined(separator: exportFormat.newLineSeparator())
case .csv:
return players().map { [$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted() ].joined(separator: exportFormat.separator()) }.joined(separator: exportFormat.separator())
case .championship:
return players().map { $0.pasteData(exportFormat) }.joined(separator: exportFormat.separator())
}
}
@ -486,7 +602,7 @@ final class TeamRegistration: ModelObject, Storable {
self.unsortedPlayers().sorted { (lhs, rhs) in
let predicates: [AreInIncreasingOrder] = [
{ $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 },
{ $0.rank ?? 0 < $1.rank ?? 0 },
{ $0.rank ?? Int.max < $1.rank ?? Int.max },
{ $0.lastName < $1.lastName},
{ $0.firstName < $1.firstName }
]
@ -642,6 +758,9 @@ final class TeamRegistration: ModelObject, Storable {
case _pointsEarned = "pointsEarned"
case _unregistered = "unregistered"
case _unregistrationDate = "unregistrationDate"
case _clubCode = "clubCode"
case _clubName = "clubName"
case _registratonMail = "registratonMail"
}
init(from decoder: Decoder) throws {
@ -672,6 +791,10 @@ final class TeamRegistration: ModelObject, Storable {
finalRanking = try container.decodeIfPresent(Int.self, forKey: ._finalRanking)
pointsEarned = try container.decodeIfPresent(Int.self, forKey: ._pointsEarned)
unregistrationDate = try container.decodeIfPresent(Date.self, forKey: ._unregistrationDate)
clubCode = try container.decodeIfPresent(String.self, forKey: ._clubCode)
clubName = try container.decodeIfPresent(String.self, forKey: ._clubName)
registratonMail = try container.decodeIfPresent(String.self, forKey: ._registratonMail)
}
func encode(to encoder: Encoder) throws {
@ -700,6 +823,9 @@ final class TeamRegistration: ModelObject, Storable {
try container.encode(pointsEarned, forKey: ._pointsEarned)
try container.encode(unregistered, forKey: ._unregistered)
try container.encode(unregistrationDate, forKey: ._unregistrationDate)
try container.encode(clubCode, forKey: ._clubCode)
try container.encode(clubName, forKey: ._clubName)
try container.encode(registratonMail, forKey: ._registratonMail)
}
func insertOnServer() {

@ -430,6 +430,14 @@ final class Tournament : ModelObject, Storable {
// MARK: - Computed Dependencies
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)
}
@ -597,9 +605,111 @@ defer {
teamPaste.append(team.pasteData(exportFormat, index + 1))
}
return teamPaste.joined(separator: exportFormat.newLineSeparator())
case .championship:
let headers = [
"Horodateur",
"alertCount",
"alertDescripion",
"poids",
"joker",
"playerCount",
"nveqCount",
"NC Non vérifié",
"Adresse e-mail",
"Code Club",
"Nom du Club",
"Catégorie",
"Numéro d'Equipe",
// "Nom du Capitaine (Il doit être licencié FFT 2025)",
// "Numéro du Téléphone",
// "E-mail",
// "Nom du Correspondant",
// "Numéro du Téléphone",
// "E-mail",
"JOUEUR 1 - Nom",
"JOUEUR 1 - Prénom",
"JOUEUR 1 - 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? {
return eventObject()?.clubObject()
}
@ -658,7 +768,7 @@ defer {
}
func availableSeeds() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -895,7 +1005,7 @@ defer {
func selectedSortedTeams() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -1002,11 +1112,28 @@ defer {
}
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 Store.main.filter { $0.tournament == self.id && $0.walkOut == false }
}
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 Store.main.filter { $0.tournament == self.id && $0.walkOut == true }
}
@ -1027,6 +1154,14 @@ defer {
}
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)
}
@ -1066,7 +1201,7 @@ defer {
//todo
func significantPlayerCount() -> Int {
return minimumPlayerPerTeam
return 6
}
func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
@ -1146,14 +1281,17 @@ defer {
previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory)
teamsToImport.append(previousTeam)
} else {
var registrationDate = team.registrationDate
var registrationDate = team.registrationDate ?? team.teamChampionship?.getRegistrationDate()
if let previousPlayer = players.first(where: { player in
let ids = team.players.compactMap({ $0.licenceId })
return ids.contains(player.licenceId!)
}), let previousTeamRegistrationDate = previousPlayer.team()?.registrationDate {
registrationDate = previousTeamRegistrationDate
}
let newTeam = addTeam(team.players, registrationDate: registrationDate, name: team.name)
let newTeam = addTeam(team.players, registrationDate: registrationDate, name: team.name ?? team.teamChampionship?.teamIndex)
newTeam.clubCode = team.teamChampionship?.clubCode
newTeam.clubName = team.teamChampionship?.clubName
newTeam.registratonMail = team.teamChampionship?.registrationMail
if isAnimation() {
if newTeam.weight == 0 {
newTeam.weight = team.index(in: teams) ?? 0
@ -1189,6 +1327,8 @@ defer {
}
func registrationIssues() async -> Int {
return 0
let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
@ -1226,7 +1366,7 @@ defer {
let start = Date()
defer {
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
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()
defer {
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
return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending)
@ -1248,7 +1388,7 @@ defer {
let start = Date()
defer {
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
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
@ -1259,7 +1399,7 @@ defer {
let start = Date()
defer {
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
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] {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
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
if let limit {
@ -1480,13 +1620,19 @@ defer {
}
}
var championshipImportKey: String {
if federalAgeCategory == .senior {
return tournamentCategory.importingRawValue
} else {
return federalAgeCategory.exportingRawValue
}
}
func updateWeights() {
let teams = self.unsortedTeams()
teams.forEach { team in
let players = team.unsortedPlayers()
players.forEach { $0.setComputedRank(in: self) }
team.setWeight(from: players, inTournamentCategory: tournamentCategory)
team.setWeight(from: players.sorted(by: \.computedRank), inTournamentCategory: tournamentCategory)
do {
try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
} catch {
@ -1501,6 +1647,15 @@ defer {
}
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 }
rankSourceDate = newDate
@ -1524,12 +1679,33 @@ defer {
let lastRankWoman = currentMonthData()?.femaleUnrankedValue
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate }
let sources = dataURLs.map { CSVParser(url: $0) }
try await unsortedPlayers().concurrentForEach { player in
let players = unsortedPlayers()
try await players.concurrentForEach { player in
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 {
return maleUnrankedValue == nil || femaleUnrankedValue == nil
}
@ -1592,7 +1768,7 @@ defer {
func availableQualifiedTeams() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
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 {
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
player.teamRegistration = team.id
}

@ -124,6 +124,15 @@ extension String {
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? {
if let intValue = Int(self) {
var value = intValue
@ -165,6 +174,15 @@ extension String {
let matches = self.matches(of: /[1-9][0-9]{5,7}/)
return matches.map { String(self[$0.range]) }
}
func isValidCodeClub(_ codeClubPrefix: Int) -> Bool {
let code = trimmed.replaceCharactersFromSet(characterSet: .whitespaces)
guard code.hasPrefix(String(codeClubPrefix)) else { return false }
guard code.count == 8 else { return false }
return true
}
}
// MARK: - FFT Source Importing
@ -200,11 +218,23 @@ extension LosslessStringConvertible {
extension String {
func createFile(_ withName: String = "temp", _ exportedFormat: ExportFormat = .rawText) -> URL {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(withName)
.appendingPathExtension(exportedFormat.suffix)
.appendingPathComponent(withName)
.appendingPathExtension(exportedFormat.suffix)
let string = self
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
}
}

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

@ -131,13 +131,15 @@ class FileImportManager {
let previousTeam: TeamRegistration?
var registrationDate: Date? = nil
var name: String? = nil
var teamChampionship: TeamChampionship?
init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, tournamentAgeCategory: FederalTournamentAge, previousTeam: TeamRegistration?, registrationDate: Date? = nil, name: String? = nil, tournament: Tournament) {
init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, tournamentAgeCategory: FederalTournamentAge, previousTeam: TeamRegistration?, registrationDate: Date? = nil, name: String? = nil, teamChampionship: TeamChampionship? = nil, tournament: Tournament) {
self.players = Set(players)
self.tournamentCategory = tournamentCategory
self.tournamentAgeCategory = tournamentAgeCategory
self.name = name
self.previousTeam = previousTeam
self.teamChampionship = teamChampionship
if players.count < 2 {
let s = players.compactMap { $0.sex?.rawValue }
var missing = tournamentCategory.mandatoryPlayerType()
@ -146,7 +148,7 @@ class FileImportManager {
missing.remove(at: index)
}
}
let significantPlayerCount = 2
let significantPlayerCount = tournament.significantPlayerCount()
let pl = players.prefix(significantPlayerCount).map { $0.computedRank }
let missingPl = (missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? ($0 == 1 ? 70_000 : 10_000) }).prefix(significantPlayerCount)
self.weight = pl.reduce(0,+) + missingPl.reduce(0,+)
@ -179,7 +181,7 @@ class FileImportManager {
static let FFT_ASSIMILATION_WOMAN_IN_MAN = "A calculer selon la pondération en vigueur"
func createTeams(from fileContent: String, tournament: Tournament, fileProvider: FileProvider = .frenchFederation, checkingCategoryDisabled: Bool, chunkByParameter: Bool) async throws -> [TeamHolder] {
func createTeams(from fileContent: String, tournament: Tournament, fileProvider: FileProvider = .frenchFederation, checkingCategoryDisabled: Bool, chunkMode: ChunkMode) async throws -> [TeamHolder] {
switch fileProvider {
case .frenchFederation:
@ -187,9 +189,9 @@ class FileImportManager {
case .padelClub:
return await _getPadelClubTeams(from: fileContent, tournament: tournament)
case .custom:
return await _getPadelBusinessLeagueTeams(from: fileContent, chunkByParameter: chunkByParameter, autoSearch: false, tournament: tournament)
return await _getPadelBusinessLeagueTeams(from: fileContent, chunkMode: chunkMode, autoSearch: false, tournament: tournament)
case .customAutoSearch:
return await _getPadelBusinessLeagueTeams(from: fileContent, chunkByParameter: chunkByParameter, autoSearch: true, tournament: tournament)
return await _getPadelBusinessLeagueTeams(from: fileContent, chunkMode: chunkMode, autoSearch: true, tournament: tournament)
}
}
@ -425,7 +427,7 @@ class FileImportManager {
return results
}
private func _getPadelBusinessLeagueTeams(from fileContent: String, chunkByParameter: Bool, autoSearch: Bool, tournament: Tournament) async -> [TeamHolder] {
private func _getPadelBusinessLeagueTeams(from fileContent: String, chunkMode: ChunkMode, autoSearch: Bool, tournament: Tournament) async -> [TeamHolder] {
let lines = fileContent.replacingOccurrences(of: "\"", with: "").components(separatedBy: "\n")
guard let firstLine = lines.first else { return [] }
var separator = ","
@ -436,13 +438,27 @@ class FileImportManager {
let federalContext = PersistenceController.shared.localContainer.viewContext
var chunks: [[String]] = []
if chunkByParameter {
switch chunkMode {
case .byParameter:
chunks = lines.chunked(byParameterAt: 1)
} else {
case .byCoupleOfLines:
chunks = lines.chunked(into: 2)
case .byColumn:
chunks = lines.extractPlayers(filterKey: tournament.championshipImportKey.capitalized, separator: separator)
}
let results = chunks.map { team in
let results = chunks.map { teamSource in
var teamName: String? = nil
var teamChampionship: TeamChampionship? = nil
var team = teamSource
if chunkMode == .byColumn {
if let first = teamSource.first?.components(separatedBy: separator) {
team = Array(teamSource.dropFirst())
teamChampionship = TeamChampionship(registrationDate: first[0], registrationMail: first[1], clubCode: first[2], teamIndex: first[3], clubName: first[4])
}
}
let players = team.map { player in
let data = player.components(separatedBy: separator)
let lastName : String = data[safe: 2]?.prefixTrimmed(50) ?? ""
@ -456,27 +472,84 @@ class FileImportManager {
let rank : Int? = data[safe: 6]?.trimmed.toInt()
let licenceId : String? = data[safe: 7]?.prefixTrimmed(50)
let club : String? = data[safe: 8]?.prefixTrimmed(200)
let predicate = NSPredicate(format: "firstName like[cd] %@ && lastName like[cd] %@", firstName, lastName)
fetchRequest.predicate = predicate
let found = try? federalContext.fetch(fetchRequest).first
if let found, autoSearch {
let player = PlayerRegistration(importedPlayer: found)
player.setComputedRank(in: tournament)
player.email = email
player.phoneNumber = phoneNumber
return player
let status : String? = data[safe: 9]
let verified : String? = data[safe: 10]
let isVerified = verified == "ok"
let isEQVerified = verified == "ok2"
if chunkMode == .byColumn {
if let licenceId = licenceId?.strippedLicense {
let predicate = NSPredicate(format: "license == %@", licenceId)
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 {
let player = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: licenceId, rank: rank, sex: sex, clubName: club, phoneNumber: phoneNumber, email: email)
if rank == nil, autoSearch {
let predicate = NSPredicate(format: "firstName like[cd] %@ && lastName like[cd] %@", firstName, lastName)
fetchRequest.predicate = predicate
let found = try? federalContext.fetch(fetchRequest).first
if let found, autoSearch {
let player = PlayerRegistration(importedPlayer: found)
player.setComputedRank(in: tournament)
player.email = email
player.phoneNumber = phoneNumber
return player
} else {
player.computedRank = rank ?? 0
let player = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: licenceId, rank: rank, sex: sex, clubName: club, phoneNumber: phoneNumber, email: email)
if rank == nil, autoSearch {
player.setComputedRank(in: tournament)
} else {
player.computedRank = rank ?? 0
}
return player
}
return player
}
}
return TeamHolder(players: players, tournamentCategory: tournament.tournamentCategory, tournamentAgeCategory: tournament.federalTournamentAge, previousTeam: nil, name: teamName, tournament: tournament)
return TeamHolder(players: players, tournamentCategory: tournament.tournamentCategory, tournamentAgeCategory: tournament.federalTournamentAge, previousTeam: nil, teamChampionship: teamChampionship, tournament: tournament)
}
return results
}
@ -513,5 +586,292 @@ extension Array where Element == String {
return groups.map { $0.value }
}
}
func extractPlayers(filterKey: String = "Messieurs", separator: String = ";") -> [[String]] {
return self.dropFirst().compactMap { line in
let components = line.components(separatedBy: separator)
guard components.count >= 62 else { return nil }
guard components.contains(filterKey) else { return nil }
var players: [PlayerChampionship] = []
let teamChampionship = TeamChampionship(registrationDate: components[0], registrationMail: components[1], clubCode: components[3], teamIndex: components[5], clubName: components[2])
// Add captain and coach first
// players.append(PlayerChampionship.captain(components))
// players.append(PlayerChampionship.coach(components))
// Extract team information
let teamType = components[4]
let sex = teamType.lowercased().contains("dames") ? "f" : "m"
// Process up to 10 players
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 {
switch self {
case .unlisted:

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

@ -18,7 +18,7 @@ struct TournamentLookUpView: View {
@State private var searchField: String = ""
@State var page: Int = 0
@State var total: Int = 0
@State private var showingSettingsAlert = false
@State private var searching: Bool = false
@State private var requestedToGetAllPages: Bool = false
@State private var revealSearchParameters: Bool = true
@ -57,6 +57,16 @@ struct TournamentLookUpView: View {
} 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.")
}
.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: {
Button {
presentAlert = false
@ -305,9 +315,15 @@ struct TournamentLookUpView: View {
}
if locationManager.requestStarted {
ProgressView()
} else {
} else if locationManager.manager.authorizationStatus != .restricted {
LocationButton {
locationManager.requestLocation()
if locationManager.manager.authorizationStatus == .notDetermined {
locationManager.manager.requestWhenInUseAuthorization()
} else if locationManager.manager.authorizationStatus == .denied {
showingSettingsAlert = true
} else {
locationManager.requestLocation()
}
}
.symbolVariant(.fill)
.foregroundColor (Color.white)
@ -485,4 +501,12 @@ struct TournamentLookUpView: View {
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 {
return
print("dataStore.appSettings.lastDataSource :", dataStore.appSettings.lastDataSource ?? "none")
print("check internet")
print("check files on internet")

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

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

@ -42,7 +42,7 @@ extension UpperRound: Equatable {
}
func badgeValue() -> Int? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -57,7 +57,7 @@ extension UpperRound: Equatable {
}
func badgeImage() -> Badge? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -85,7 +85,7 @@ struct LoserRound: Identifiable, Selectable {
}
var shouldBeDisplayed: Bool {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
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] {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -146,7 +146,7 @@ extension LoserRound: Equatable {
}
func badgeImage() -> Badge? {
#if _DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)

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

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

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

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

@ -52,6 +52,13 @@ struct InscriptionManagerView: View {
@State private var refreshResult: String? = nil
@State private var refreshInProgress: Bool = false
@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 {
return self.tournament.tournamentStore
@ -186,14 +193,18 @@ struct InscriptionManagerView: View {
}
private func _setHash() {
#if _DEBUG_TIME //DEBUGING TIME
self.selectedSortedTeams = self.tournament.selectedSortedTeams()
return
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let selectedSortedTeams = tournament.selectedSortedTeams()
//let selectedSortedTeams = tournament.selectedSortedTeams()
if self.teamsHash == nil, selectedSortedTeams.isEmpty == false {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
}
@ -204,7 +215,7 @@ struct InscriptionManagerView: View {
}
private func _handleHashDiff() {
let selectedSortedTeams = tournament.selectedSortedTeams()
//let selectedSortedTeams = tournament.selectedSortedTeams()
let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) {
self.teamsHash = newHash
@ -461,15 +472,19 @@ struct InscriptionManagerView: View {
private func _sharingTeamsMenuView() -> some View {
Menu {
if let teamPaste = teamPaste() {
ShareLink(item: teamPaste) {
Text("En texte")
}
}
if let teamPaste = teamPaste(.csv) {
ShareLink(item: teamPaste) {
Text("En csv")
}
// if let teamPaste = teamPaste() {
// ShareLink(item: teamPaste) {
// Text("En texte")
// }
// }
// if let teamPaste = teamPaste(.csv) {
// ShareLink(item: teamPaste) {
// Text("En csv")
// }
// }
ShareLink(item: teamPaste(.championship), preview: .init("championship")) {
Text("championship")
}
} label: {
Label("Exporter les paires", systemImage: "square.and.arrow.up")
@ -484,8 +499,8 @@ struct InscriptionManagerView: View {
tournament.unsortedTeamsWithoutWO()
}
func teamPaste(_ exportFormat: ExportFormat = .rawText) -> URL? {
tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle(.short), exportFormat)
func teamPaste(_ exportFormat: ExportFormat = .rawText) -> TournamentShareFile {
TournamentShareFile(tournament: tournament, exportFormat: exportFormat)
}
var unsortedPlayers: [PlayerRegistration] {
@ -494,9 +509,9 @@ struct InscriptionManagerView: View {
var sortedTeams: [TeamRegistration] {
if filterMode == .waiting {
return tournament.waitingListSortedTeams()
return tournament.waitingListTeams(in: selectedSortedTeams, includingWalkOuts: false)
} else {
return tournament.sortedTeams()
return selectedSortedTeams + tournament.waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true)
}
}
@ -575,7 +590,7 @@ struct InscriptionManagerView: View {
private func _teamRegisteredView() -> some View {
List {
let selectedSortedTeams = tournament.selectedSortedTeams()
let sortedTeams = sortedTeams
if presentSearch == false {
_informationView()
@ -619,7 +634,7 @@ struct InscriptionManagerView: View {
EditingTeamView(team: team)
.environment(tournament)
} label: {
TeamRowView(team: team)
TeamRowView(team: team, teamIndex: teamIndex)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
_teamDeleteButtonView(team)
@ -725,15 +740,15 @@ struct InscriptionManagerView: View {
private func _teamCountForFilterMode(filterMode: FilterMode) -> String {
switch filterMode {
case .wildcardBracket:
return tournament.selectedSortedTeams().filter({ $0.wildCardBracket }).count.formatted()
return selectedSortedTeams.filter({ $0.wildCardBracket }).count.formatted()
case .wildcardGroupStage:
return tournament.selectedSortedTeams().filter({ $0.wildCardGroupStage }).count.formatted()
return selectedSortedTeams.filter({ $0.wildCardGroupStage }).count.formatted()
case .all:
return unsortedTeamsWithoutWO.count.formatted()
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:
return tournament.selectedSortedTeams().filter({ $0.inGroupStage() }).count.formatted()
return selectedSortedTeams.filter({ $0.inGroupStage() }).count.formatted()
case .walkOut:
let wo = walkoutTeams.count.formatted()
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: {
HStack {
Spacer()
@ -1124,3 +1294,20 @@ struct InscriptionManagerView: View {
// .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