Merge branch 'main'

sync3
Razmig Sarkissian 3 months ago
commit ed06b68405
  1. 24
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift
  3. 59
      PadelClub/Data/Federal/FederalPlayer.swift
  4. 9
      PadelClub/Data/Federal/FederalTournament.swift
  5. 16
      PadelClub/Extensions/MonthData+Extensions.swift
  6. 11
      PadelClub/Extensions/TeamRegistration+Extensions.swift
  7. 4
      PadelClub/Extensions/Tournament+Extensions.swift
  8. 1
      PadelClub/HTML Templates/match-template.html
  9. 8
      PadelClub/HTML Templates/tournament-template.html
  10. 9
      PadelClub/Utils/HtmlGenerator.swift
  11. 105
      PadelClub/Utils/HtmlService.swift
  12. 368
      PadelClub/Utils/Network/FederalDataService.swift
  13. 205
      PadelClub/Utils/Network/NetworkFederalService.swift
  14. 59
      PadelClub/Utils/PhoneNumbersUtils.swift
  15. 4
      PadelClub/ViewModel/FederalDataViewModel.swift
  16. 18
      PadelClub/Views/Calling/Components/MenuWarningView.swift
  17. 16
      PadelClub/Views/Cashier/Event/EventSettingsView.swift
  18. 3
      PadelClub/Views/Cashier/Event/EventView.swift
  19. 12
      PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift
  20. 23
      PadelClub/Views/Club/ClubSearchView.swift
  21. 4
      PadelClub/Views/Match/EditSharingView.swift
  22. 7
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  23. 20
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  24. 127
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  25. 20
      PadelClub/Views/Navigation/Umpire/PadelClubView.swift
  26. 43
      PadelClub/Views/Planning/PlanningView.swift
  27. 115
      PadelClub/Views/Player/PlayerDetailView.swift
  28. 2
      PadelClub/Views/Team/EditingTeamView.swift
  29. 48
      PadelClub/Views/Tournament/FileImportView.swift
  30. 11
      PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift
  31. 1
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  32. 134
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  33. 10
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  34. 6
      PadelClubTests/ServerDataTests.swift

@ -650,6 +650,9 @@
FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; };
FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; };
FF8044AC2C8F676D00A49A52 /* TournamentSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */; };
FF81F1BC2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */; };
FF81F1BD2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */; };
FF81F1BE2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */; };
FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */; };
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */; };
FF8E1CE62C006E0200184680 /* Alphabet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8E1CE52C006E0200184680 /* Alphabet.swift */; };
@ -750,6 +753,9 @@
FFCFC01A2BBC5A8500B82851 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; };
FFD655D82C8DE27400E5B35E /* TournamentLookUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */; };
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; };
FFD883792E1E63880004D7DD /* FederalDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD883782E1E63880004D7DD /* FederalDataService.swift */; };
FFD8837A2E1E63880004D7DD /* FederalDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD883782E1E63880004D7DD /* FederalDataService.swift */; };
FFD8837B2E1E63880004D7DD /* FederalDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD883782E1E63880004D7DD /* FederalDataService.swift */; };
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; };
FFE103082C353B7600684FC9 /* EventClubSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE103072C353B7600684FC9 /* EventClubSettingsView.swift */; };
FFE103102C366DCD00684FC9 /* EditSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE1030F2C366DCD00684FC9 /* EditSharingView.swift */; };
@ -1058,6 +1064,7 @@
FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageDatePickingView.swift; sourceTree = "<group>"; };
FF7DCD382CC330260041110C /* TeamRestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRestingView.swift; sourceTree = "<group>"; };
FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSubscriptionView.swift; sourceTree = "<group>"; };
FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumbersUtils.swift; sourceTree = "<group>"; };
FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = "<group>"; };
FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
FF8E1CE52C006E0200184680 /* Alphabet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alphabet.swift; sourceTree = "<group>"; };
@ -1140,6 +1147,7 @@
FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLookUpView.swift; sourceTree = "<group>"; };
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = "<group>"; };
FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
FFD883782E1E63880004D7DD /* FederalDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalDataService.swift; sourceTree = "<group>"; };
FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = "<group>"; };
FFE103072C353B7600684FC9 /* EventClubSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EventClubSettingsView.swift; path = PadelClub/Views/Tournament/Screen/Components/EventClubSettingsView.swift; sourceTree = SOURCE_ROOT; };
FFE1030F2C366DCD00684FC9 /* EditSharingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSharingView.swift; sourceTree = "<group>"; };
@ -1739,6 +1747,7 @@
FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */,
FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */,
FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */,
FFD883782E1E63880004D7DD /* FederalDataService.swift */,
);
path = Network;
sourceTree = "<group>";
@ -1935,6 +1944,7 @@
FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */,
FF1DC5582BAB767000FD8220 /* Tips.swift */,
C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */,
FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -2353,6 +2363,7 @@
FF5D30512BD94E1000F2B93D /* ImportedPlayer+Extensions.swift in Sources */,
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */,
FFE8B5B72DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */,
FFD8837A2E1E63880004D7DD /* FederalDataService.swift in Sources */,
FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
@ -2431,6 +2442,7 @@
FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */,
FFCB74172C480411008384D0 /* CopyPasteButtonView.swift in Sources */,
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */,
FF81F1BD2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */,
FF1F4B712BF9EFE9000B4573 /* TournamentInscriptionView.swift in Sources */,
FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */,
FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */,
@ -2616,6 +2628,7 @@
FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */,
FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FFD883792E1E63880004D7DD /* FederalDataService.swift in Sources */,
FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */,
FF4CBFDE2C996C0600151637 /* CalendarView.swift in Sources */,
FF4CBFDF2C996C0600151637 /* FederalTournamentSearchScope.swift in Sources */,
@ -2694,6 +2707,7 @@
FF4CC0252C996C0600151637 /* Labels.swift in Sources */,
FF4CC0262C996C0600151637 /* CopyPasteButtonView.swift in Sources */,
FF4CC0272C996C0600151637 /* PlayerSexPickerView.swift in Sources */,
FF81F1BE2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */,
FF4CC0282C996C0600151637 /* TournamentInscriptionView.swift in Sources */,
FF4CC0292C996C0600151637 /* CallSettingsView.swift in Sources */,
FF4CC02A2C996C0600151637 /* FooterButtonView.swift in Sources */,
@ -2857,6 +2871,7 @@
FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */,
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FFD8837B2E1E63880004D7DD /* FederalDataService.swift in Sources */,
FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */,
FF70FB5D2C90584900129CC2 /* CalendarView.swift in Sources */,
FF70FB5E2C90584900129CC2 /* FederalTournamentSearchScope.swift in Sources */,
@ -2935,6 +2950,7 @@
FF70FBA42C90584900129CC2 /* Labels.swift in Sources */,
FF70FBA52C90584900129CC2 /* CopyPasteButtonView.swift in Sources */,
FF70FBA62C90584900129CC2 /* PlayerSexPickerView.swift in Sources */,
FF81F1BC2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */,
FF70FBA72C90584900129CC2 /* TournamentInscriptionView.swift in Sources */,
FF70FBA82C90584900129CC2 /* CallSettingsView.swift in Sources */,
FF70FBA92C90584900129CC2 /* FooterButtonView.swift in Sources */,
@ -3101,7 +3117,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\"";
@ -3128,7 +3144,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.40;
MARKETING_VERSION = 1.2.46;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3149,7 +3165,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;
@ -3174,7 +3190,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.40;
MARKETING_VERSION = 1.2.46;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

@ -132,6 +132,6 @@ extension ImportedPlayer: PlayerHolder {
fileprivate extension Int {
var femaleInMaleAssimilation: Int {
self + TournamentCategory.femaleInMaleAssimilationAddition(self)
self + TournamentCategory.femaleInMaleAssimilationAddition(self, seasonYear: Date.now.seasonYear())
}
}

@ -31,54 +31,70 @@ class FederalPlayer: Decodable {
}
required init(from decoder: Decoder) throws {
/*
"classement": 9,
"evolution": 2,
"nom": "PEREZ LE TIEC",
"prenom": "Pierre",
"meilleurClassement": null,
"nationalite": "FRA",
"ageSportif": 30,
"points": 14210,
"nombreTournoisJoues": 24,
"ligue": "ILE DE FRANCE",
"assimilation": false
*/
enum CodingKeys: String, CodingKey {
case nom
case prenom
case licence
case meilleurClassement
case nationnalite
case anneeNaissance
case nationalite
case codeClub
case nomClub
case nomLigue
case rang
case progression
case ligue
case classement
case evolution
case points
case nombreDeTournois
case assimile
case nombreTournoisJoues
case assimilation
case ageSportif
}
let container = try decoder.container(keyedBy: CodingKeys.self)
isMale = (decoder.userInfo[.maleData] as? Bool) == true
let _lastName = try container.decode(String.self, forKey: .nom)
let _firstName = try container.decode(String.self, forKey: .prenom)
lastName = _lastName
firstName = _firstName
let _lastName = try container.decodeIfPresent(String.self, forKey: .nom)
let _firstName = try container.decodeIfPresent(String.self, forKey: .prenom)
lastName = _lastName ?? ""
firstName = _firstName ?? ""
if let lic = try? container.decodeIfPresent(Int.self, forKey: .licence) {
license = String(lic)
} else {
license = ""
}
let nationnalite = try container.decode(Nationnalite.self, forKey: .nationnalite)
country = nationnalite.code
country = try container.decodeIfPresent(String.self, forKey: .nationalite) ?? ""
bestRank = try container.decodeIfPresent(Int.self, forKey: .meilleurClassement)
birthYear = try container.decodeIfPresent(Int.self, forKey: .anneeNaissance)
clubCode = try container.decode(String.self, forKey: .codeClub)
club = try container.decode(String.self, forKey: .nomClub)
ligue = try container.decode(String.self, forKey: .nomLigue)
rank = try container.decode(Int.self, forKey: .rang)
progression = (try? container.decodeIfPresent(Int.self, forKey: .progression)) ?? 0
let ageSportif = try container.decodeIfPresent(Int.self, forKey: .ageSportif)
if let ageSportif {
birthYear = Calendar.current.component(.year, from: Date()) - ageSportif
}
clubCode = try container.decodeIfPresent(String.self, forKey: .codeClub) ?? ""
club = try container.decodeIfPresent(String.self, forKey: .nomClub) ?? ""
ligue = try container.decodeIfPresent(String.self, forKey: .ligue) ?? ""
rank = try container.decode(Int.self, forKey: .classement)
progression = (try? container.decodeIfPresent(Int.self, forKey: .evolution)) ?? 0
let pointsAsInt = try? container.decodeIfPresent(Int.self, forKey: .points)
if let pointsAsInt {
points = Double(pointsAsInt)
} else {
points = nil
}
tournamentCount = try? container.decodeIfPresent(Int.self, forKey: .nombreDeTournois)
let assimile = try container.decode(Bool.self, forKey: .assimile)
tournamentCount = try? container.decodeIfPresent(Int.self, forKey: .nombreTournoisJoues)
let assimile = try container.decode(Bool.self, forKey: .assimilation)
assimilation = assimile ? "Oui" : "Non"
}
@ -92,6 +108,7 @@ class FederalPlayer: Decodable {
}
func formatNumbers(_ input: String) -> String {
if input.isEmpty { return input }
// Insert spaces at appropriate positions
let formattedString = insertSeparator(input, separator: " ", every: [2, 4])
return formattedString

@ -341,17 +341,10 @@ struct CategoriesAgeTypePratique: Codable {
// MARK: - ID
struct ID: Codable {
var typePratique: TypePratique?
var typePratique: String?
var idCategorieAge: Int?
}
enum TypePratique: String, Codable {
case beach = "BEACH"
case padel = "PADEL"
case tennis = "TENNIS"
case pickle = "PICKLE"
}
// MARK: - CategorieTournoi
struct CategorieTournoi: Codable {
var code, codeTaxe: String?

@ -15,13 +15,22 @@ extension MonthData {
let fileURL = SourceFileManager.shared.allFiles(true).first(where: { $0.dateFromPath == fromDate && $0.index == 0 })
print("calculateCurrentUnrankedValues", fromDate.monthYearFormatted, fileURL?.path())
let fftImportingUncomplete = fileURL?.fftImportingUncomplete()
var fftImportingAnonymous = fileURL?.fftImportingAnonymous()
let fftImportingMaleUnrankValue = fileURL?.fftImportingMaleUnrankValue()
let femaleFileURL = SourceFileManager.shared.allFiles(false).first(where: { $0.dateFromPath == fromDate && $0.index == 0 })
let femaleFftImportingMaleUnrankValue = femaleFileURL?.fftImportingMaleUnrankValue()
let femaleFftImportingUncomplete = femaleFileURL?.fftImportingUncomplete()
let incompleteMode = fftImportingUncomplete != nil
let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: fromDate, man: true)
let lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: fromDate, man: false)
let anonymousCount = await FederalPlayer.anonymousCount(mostRecentDateAvailable: fromDate)
if fftImportingAnonymous == nil {
fftImportingAnonymous = await FederalPlayer.anonymousCount(mostRecentDateAvailable: fromDate)
}
let anonymousCount: Int? = fftImportingAnonymous
await MainActor.run {
let lastDataSource = URL.importDateFormatter.string(from: fromDate)
let currentMonthData : MonthData = DataStore.shared.monthData.first(where: { $0.monthKey == lastDataSource }) ?? MonthData(monthKey: lastDataSource)
@ -30,11 +39,10 @@ extension MonthData {
currentMonthData.maleUnrankedValue = incompleteMode ? fftImportingMaleUnrankValue : lastDataSourceMaleUnranked?.0
currentMonthData.incompleteMode = incompleteMode
currentMonthData.maleCount = incompleteMode ? fftImportingUncomplete : lastDataSourceMaleUnranked?.1
currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked?.0
currentMonthData.femaleCount = lastDataSourceFemaleUnranked?.1
currentMonthData.femaleUnrankedValue = incompleteMode ? femaleFftImportingMaleUnrankValue : lastDataSourceFemaleUnranked?.0
currentMonthData.femaleCount = incompleteMode ? femaleFftImportingUncomplete : lastDataSourceFemaleUnranked?.1
currentMonthData.anonymousCount = anonymousCount
DataStore.shared.monthData.addOrUpdate(instance: currentMonthData)
}
}

@ -39,6 +39,17 @@ extension TeamRegistration {
player.licenceId?.strippedLicense != nil
{
player.registeredOnline = oldPlayer.registeredOnline
if player.email?.canonicalVersion != oldPlayer.email?.canonicalVersion {
player.contactEmail = oldPlayer.email
} else {
player.contactEmail = oldPlayer.contactEmail
}
if areFrenchPhoneNumbersSimilar(player.phoneNumber, oldPlayer.phoneNumber) == false {
player.contactPhoneNumber = oldPlayer.phoneNumber
} else {
player.contactPhoneNumber = oldPlayer.contactPhoneNumber
}
player.contactName = oldPlayer.contactName
player.coach = oldPlayer.coach
player.tournamentPlayed = oldPlayer.tournamentPlayed
player.points = oldPlayer.points

@ -172,8 +172,8 @@ extension Tournament {
func isPlayerRankInadequate(player: PlayerHolder) -> Bool {
guard let rank = player.getRank() else { return false }
let _rank = player.male ? rank : rank + PlayerRegistration.addon(for: rank, manMax: maleUnrankedValue ?? 0, womanMax: femaleUnrankedValue ?? 0)
if _rank <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge) {
let _rank = player.male ? rank : rank + addon(for: rank, manMax: maleUnrankedValue ?? 0, womanMax: femaleUnrankedValue ?? 0)
if _rank <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge, seasonYear: startDate.seasonYear()) {
return true
} else {
return false

@ -3,6 +3,7 @@
<div class="match-description-overlay" style="visibility:{{hidden}};">{{matchDescriptionTop}}</div>
</li>
<li class="game game-spacer" style="visibility:{{hidden}}">
<div class="center-match-overlay" style="visibility:{{hidden}};">{{centerMatchText}}</div>
</li>
<li class="game game-bottom {{entrantTwoWon}}" style="visibility:{{hidden}}; position: relative;">
<div style="transform: translateY(-100%);">

@ -113,6 +113,14 @@
font-size: 1em; /* Optional: Adjust font size */
/* Add any other desired styling for the overlay */
}
.center-match-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.8em;
white-space: nowrap; /* Prevents text from wrapping */
}
</style>
</head>
<body>

@ -27,6 +27,7 @@ class HtmlGenerator: ObservableObject {
@Published var displayRank: Bool = false
@Published var displayTeamIndex: Bool = false
@Published var displayScore: Bool = false
@Published var displayPlannedDate: Bool = true
private var pdfDocument: PDFDocument = PDFDocument()
private var rects: [CGRect] = []
@ -179,12 +180,16 @@ class HtmlGenerator: ObservableObject {
func generateHtml() -> String {
//HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html()
HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore)
HtmlService.template(tournament: tournament).html(options: options)
}
func generateLoserBracketHtml(upperRound: Round) -> String {
//HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html()
HtmlService.loserBracket(upperRound: upperRound, hideTitle: false).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore)
HtmlService.loserBracket(upperRound: upperRound, hideTitle: false).html(options: options)
}
var options: HtmlOptions {
HtmlOptions(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore, withPlannedDate: displayPlannedDate, includeLoserBracket: includeLoserBracket)
}
var pdfURL: URL? {

@ -8,6 +8,32 @@
import Foundation
import PadelClubData
struct HtmlOptions {
let headName: Bool
let withRank: Bool
let withTeamIndex: Bool
let withScore: Bool
let withPlannedDate: Bool
let includeLoserBracket: Bool
// Default initializer with all options defaulting to true
init(
headName: Bool = true,
withRank: Bool = true,
withTeamIndex: Bool = true,
withScore: Bool = true,
withPlannedDate: Bool = true,
includeLoserBracket: Bool = false
) {
self.headName = headName
self.withRank = withRank
self.withTeamIndex = withTeamIndex
self.withScore = withScore
self.withPlannedDate = withPlannedDate
self.includeLoserBracket = includeLoserBracket
}
}
enum HtmlService {
case template(tournament: Tournament)
@ -51,7 +77,7 @@ enum HtmlService {
}
}
func html(headName: Bool, withRank: Bool, withTeamIndex: Bool, withScore: Bool) -> String {
func html(options: HtmlOptions = HtmlOptions()) -> String {
guard let file = Bundle.main.path(forResource: self.fileName, ofType: "html") else {
fatalError()
}
@ -74,8 +100,8 @@ enum HtmlService {
var col = ""
var row = ""
bracket.teams().forEach { entrant in
col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(options: options))
row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(options: options))
}
template = template.replacingOccurrences(of: "{{teamsCol}}", with: col)
template = template.replacingOccurrences(of: "{{teamsRow}}", with: row)
@ -83,7 +109,7 @@ enum HtmlService {
return template
case .groupstageEntrant(let entrant):
var template = html
if withTeamIndex == false {
if options.withTeamIndex == false {
template = template.replacingOccurrences(of: #"<div class="player">{{teamIndex}}</div>"#, with: "")
} else {
template = template.replacingOccurrences(of: "{{teamIndex}}", with: entrant.seedIndex() ?? "")
@ -91,7 +117,7 @@ enum HtmlService {
if let playerOne = entrant.players()[safe: 0] {
template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel())
if withRank {
if options.withRank {
template = template.replacingOccurrences(of: "{{weightOne}}", with: "(\(playerOne.formattedRank()))")
} else {
template = template.replacingOccurrences(of: "{{weightOne}}", with: "")
@ -103,7 +129,7 @@ enum HtmlService {
if let playerTwo = entrant.players()[safe: 1] {
template = template.replacingOccurrences(of: "{{playerTwo}}", with: playerTwo.playerLabel())
if withRank {
if options.withRank {
template = template.replacingOccurrences(of: "{{weightTwo}}", with: "(\(playerTwo.formattedRank()))")
} else {
template = template.replacingOccurrences(of: "{{weightTwo}}", with: "")
@ -115,7 +141,7 @@ enum HtmlService {
return template
case .groupstageRow(let entrant, let teamsPerBracket):
var template = html
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(options: options))
var scores = ""
(0..<teamsPerBracket).forEach { index in
@ -124,18 +150,18 @@ enum HtmlService {
if shouldHide == false {
match = entrant.groupStageObject()?.matchPlayed(by: entrant.groupStagePosition!, againstPosition: index)
}
scores.append(HtmlService.groupstageScore(score: match, shouldHide: shouldHide).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
scores.append(HtmlService.groupstageScore(score: match, shouldHide: shouldHide).html(options: options))
}
template = template.replacingOccurrences(of: "{{scores}}", with: scores)
return template
case .groupstageColumn(let entrant, let position):
var template = html
template = template.replacingOccurrences(of: "{{tablePosition}}", with: position)
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageEntrant(entrant: entrant).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageEntrant(entrant: entrant).html(options: options))
return template
case .groupstageScore(let match, let shouldHide):
var template = html
if match == nil || withScore == false {
if match == nil || options.withScore == false {
template = template.replacingOccurrences(of: "{{winner}}", with: "")
template = template.replacingOccurrences(of: "{{score}}", with: "")
} else if let match, let winner = match.winner() {
@ -146,7 +172,7 @@ enum HtmlService {
return template
case .player(let entrant):
var template = html
if withTeamIndex == false {
if options.withTeamIndex == false {
template = template.replacingOccurrences(of: #"<div class="player">{{teamIndex}}</div>"#, with: "")
} else {
template = template.replacingOccurrences(of: "{{teamIndex}}", with: entrant.formattedSeed())
@ -155,7 +181,7 @@ enum HtmlService {
if let playerOne = entrant.players()[safe: 0] {
template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel())
if withRank {
if options.withRank {
template = template.replacingOccurrences(of: "{{weightOne}}", with: "(\(playerOne.formattedRank()))")
} else {
template = template.replacingOccurrences(of: "{{weightOne}}", with: "")
@ -167,7 +193,7 @@ enum HtmlService {
if let playerTwo = entrant.players()[safe: 1] {
template = template.replacingOccurrences(of: "{{playerTwo}}", with: playerTwo.playerLabel())
if withRank {
if options.withRank {
template = template.replacingOccurrences(of: "{{weightTwo}}", with: "(\(playerTwo.formattedRank()))")
} else {
template = template.replacingOccurrences(of: "{{weightTwo}}", with: "")
@ -179,27 +205,32 @@ enum HtmlService {
return template
case .hiddenPlayer:
var template = html + html
if withTeamIndex {
if options.withTeamIndex {
template += html
}
return template
case .match(let match):
var template = html
if options.withPlannedDate, let plannedStartDate = match.plannedStartDate {
template = template.replacingOccurrences(of: "{{centerMatchText}}", with: plannedStartDate.localizedDate())
} else {
}
if let entrantOne = match.team(.one) {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
if withScore, let top = match.topPreviousRoundMatch(), top.hasEnded() {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(options: options))
if options.withScore, let top = match.topPreviousRoundMatch(), top.hasEnded() {
template = template.replacingOccurrences(of: "{{matchDescriptionTop}}", with: [top.scoreLabel(winnerFirst:true)].compactMap({ $0 }).joined(separator: "\n"))
}
} else {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(options: options))
}
if let entrantTwo = match.team(.two) {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
if withScore, let bottom = match.bottomPreviousRoundMatch(), bottom.hasEnded() {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(options: options))
if options.withScore, let bottom = match.bottomPreviousRoundMatch(), bottom.hasEnded() {
template = template.replacingOccurrences(of: "{{matchDescriptionBottom}}", with: [bottom.scoreLabel(winnerFirst:true)].compactMap({ $0 }).joined(separator: "\n"))
}
} else {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(options: options))
}
if match.disabled {
template = template.replacingOccurrences(of: "{{hidden}}", with: "hidden")
@ -216,12 +247,13 @@ enum HtmlService {
}
template = template.replacingOccurrences(of: "{{matchDescriptionTop}}", with: "")
template = template.replacingOccurrences(of: "{{matchDescriptionBottom}}", with: "")
template = template.replacingOccurrences(of: "{{centerMatchText}}", with: "")
return template
case .bracket(let round):
var template = ""
var bracket = ""
for (_, match) in round._matches().enumerated() {
template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
template = template.appending(HtmlService.match(match: match).html(options: options))
}
bracket = html.replacingOccurrences(of: "{{match-template}}", with: template)
@ -230,7 +262,7 @@ enum HtmlService {
return bracket
case .loserBracket(let upperRound, let hideTitle):
var template = html
template = template.replacingOccurrences(of: "{{minHeight}}", with: withTeamIndex ? "226" : "156")
template = template.replacingOccurrences(of: "{{minHeight}}", with: options.withTeamIndex ? "226" : "156")
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: upperRound.correspondingLoserRoundTitle())
if let tournamentStartDate = upperRound.initialStartDate()?.localizedDate() {
template = template.replacingOccurrences(of: "{{tournamentStartDate}}", with: tournamentStartDate)
@ -242,10 +274,10 @@ enum HtmlService {
var brackets = ""
for round in upperRound.loserRounds() {
brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
brackets = brackets.appending(HtmlService.bracket(round: round).html(options: options))
if round.index == 1 {
let sub = HtmlService.loserBracket(upperRound: round, hideTitle: true).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)
let sub = HtmlService.loserBracket(upperRound: round, hideTitle: true).html(options: options)
template = template.appending(sub)
}
}
@ -265,7 +297,7 @@ enum HtmlService {
for round in upperRound.loserRounds() {
if round.index > 1 {
let sub = HtmlService.loserBracket(upperRound: round, hideTitle: true).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)
let sub = HtmlService.loserBracket(upperRound: round, hideTitle: true).html(options: options)
template = template.appending(sub)
}
}
@ -273,17 +305,24 @@ enum HtmlService {
return template
case .template(let tournament):
var template = html
template = template.replacingOccurrences(of: "{{minHeight}}", with: withTeamIndex ? "226" : "156")
template = template.replacingOccurrences(of: "{{minHeight}}", with: options.withTeamIndex ? "226" : "156")
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.title))
template = template.replacingOccurrences(of: "{{tournamentStartDate}}", with: tournament.formattedDate())
var brackets = ""
for round in tournament.rounds() {
brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
brackets = brackets.appending(HtmlService.bracket(round: round).html(options: options))
if options.includeLoserBracket {
if round.index == 1 {
let sub = HtmlService.loserBracket(upperRound: round, hideTitle: true).html(options: options)
template = template.appending(sub)
}
}
}
var winnerName = ""
if let tournamentWinner = tournament.tournamentWinner() {
winnerName = HtmlService.player(entrant: tournamentWinner).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)
winnerName = HtmlService.player(entrant: tournamentWinner).html(options: options)
}
let winner = """
<ul class="round" scope="last">
@ -297,6 +336,16 @@ enum HtmlService {
brackets = brackets.appending(winner)
template = template.replacingOccurrences(of: "{{brackets}}", with: brackets)
if options.includeLoserBracket {
for round in tournament.rounds() {
if round.index > 1 {
let sub = HtmlService.loserBracket(upperRound: round, hideTitle: true).html(options: options)
template = template.appending(sub)
}
}
}
return template
}
}

@ -0,0 +1,368 @@
//
// FederalDataService.swift
// PadelClub
//
// Created by Razmig Sarkissian on 09/07/2025.
//
import Foundation
import CoreLocation
import LeStorage
import PadelClubData
struct UmpireContactInfo: Codable {
let name: String?
let email: String?
let phone: String?
}
/// Response model for the batch umpire data endpoint
struct UmpireDataResponse: Codable {
let results: [String: UmpireContactInfo]
}
// New struct for the response from get_fft_club_tournaments and get_fft_all_tournaments
struct TournamentsAPIResponse: Codable {
let success: Bool
let tournaments: [FederalTournament]
let totalResults: Int
let currentCount: Int
let pagesScraped: Int? // Optional, as it might not always be present or relevant
let page: Int? // Optional, as it might not always be present or relevant
let umpireDataIncluded: Bool? // Only for get_fft_club_tournaments_with_umpire_data
let message: String
private enum CodingKeys: String, CodingKey {
case success
case tournaments
case totalResults = "total_results"
case currentCount = "current_count"
case pagesScraped = "pages_scraped"
case page
case umpireDataIncluded = "umpire_data_included"
case message
}
}
// MARK: - FederalDataService
/// `FederalDataService` handles all API calls related to federal data (clubs, tournaments, umpire info).
/// All direct interactions with `tenup.fft.fr` are now assumed to be handled by your backend.
class FederalDataService {
static let shared: FederalDataService = FederalDataService()
// The 'formId', 'tenupJsonDecoder', 'runTenupTask', and 'getNewBuildForm'
// from the legacy NetworkFederalService are removed as their logic is now
// handled server-side.
/// Fetches federal clubs based on geographic criteria.
/// - Parameters:
/// - country: The country code (e.g., "fr").
/// - city: The city name or address for search.
/// - radius: The search radius in kilometers.
/// - location: Optional `CLLocation` for user's precise position to calculate distance.
/// - Returns: A `FederalClubResponse` object containing a list of clubs and total count.
/// - Throws: An error if the network request fails or decoding the response is unsuccessful.
func federalClubs(country: String = "fr", city: String, radius: Double, location: CLLocation? = nil) async throws -> FederalClubResponse {
let service = try StoreCenter.main.service()
// Construct query parameters for your backend API
var queryItems: [URLQueryItem] = [
URLQueryItem(name: "country", value: country),
URLQueryItem(name: "city", value: city),
URLQueryItem(name: "radius", value: String(Int(radius)))
]
if let location = location {
queryItems.append(URLQueryItem(name: "lat", value: location.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us")))))
queryItems.append(URLQueryItem(name: "lng", value: location.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us")))))
}
// Build the URL with query parameters
var urlComponents = URLComponents()
urlComponents.queryItems = queryItems
let queryString = urlComponents.query ?? ""
// The servicePath now points to your backend's endpoint for federal clubs: 'fft/federal-clubs/'
let urlRequest = try service._baseRequest(servicePath: "fft/federal-clubs?\(queryString)", method: .get, requiresToken: false)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse) // Keep URLError for generic network issues
}
guard !data.isEmpty else {
throw NetworkManagerError.noDataReceived
}
do {
return try JSONDecoder().decode(FederalClubResponse.self, from: data)
} catch {
print("Decoding error for FederalClubResponse: \(error)")
// Map decoding error to a generic API error
throw NetworkManagerError.apiError("Failed to decode FederalClubResponse: \(error.localizedDescription)")
}
}
/// Fetches federal tournaments for a specific club.
/// This function now calls your backend, which in turn handles the `form_build_id` and pagination.
/// The `tournaments` parameter is maintained for signature compatibility but is not used for server-side fetching.
/// Client-side accumulation of results from multiple pages should be handled by the caller.
/// - Parameters:
/// - page: The current page number for pagination.
/// - tournaments: An array of already gathered tournaments (for signature compatibility; not used internally for fetching).
/// - club: The name of the club.
/// - codeClub: The unique code of the club.
/// - startDate: Optional start date for filtering tournaments.
/// - endDate: Optional end date for filtering tournaments.
/// - Returns: An array of `FederalTournament` objects for the requested page.
/// - Throws: An error if the network request fails or decoding the response is unsuccessful.
func getClubFederalTournaments(page: Int, tournaments: [FederalTournament], club: String, codeClub: String, startDate: Date? = nil, endDate: Date? = nil) async throws -> TournamentsAPIResponse {
let service = try StoreCenter.main.service()
// Construct query parameters for your backend API
var queryItems: [URLQueryItem] = [
URLQueryItem(name: "club_code", value: codeClub),
URLQueryItem(name: "club_name", value: club),
URLQueryItem(name: "page", value: String(page))
]
if let startDate = startDate {
queryItems.append(URLQueryItem(name: "start_date", value: startDate.twoDigitsYearFormatted))
}
if let endDate = endDate {
queryItems.append(URLQueryItem(name: "end_date", value: endDate.twoDigitsYearFormatted))
}
// Build the URL with query parameters
var urlComponents = URLComponents()
urlComponents.queryItems = queryItems
let queryString = urlComponents.query ?? ""
// The servicePath now points to your backend's endpoint for club tournaments: 'fft/club-tournaments/'
let urlRequest = try service._baseRequest(servicePath: "fft/club-tournaments?\(queryString)", method: .get, requiresToken: false)
print(urlRequest.url?.absoluteString)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard !data.isEmpty else {
throw NetworkManagerError.noDataReceived
}
do {
// Your backend should return a direct array of FederalTournament for the requested page
let federalTournaments = try JSONDecoder().decode(TournamentsAPIResponse.self, from: data)
return federalTournaments
} catch {
print("Decoding error for FederalTournament array: \(error)")
throw NetworkManagerError.apiError("Failed to decode FederalTournament array: \(error.localizedDescription)")
}
}
/// Fetches all federal tournaments based on various filtering options.
/// This function now calls your backend, which handles the complex filtering and data retrieval.
/// The return type `[HttpCommand]` is maintained for signature compatibility,
/// wrapping the actual `[FederalTournament]` data within an `HttpCommand` structure.
/// - Parameters:
/// - sortingOption: How to sort the results (e.g., "dateDebut asc").
/// - page: The current page number for pagination.
/// - startDate: The start date for the tournament search.
/// - endDate: The end date for the tournament search.
/// - city: The city to search within.
/// - distance: The search distance from the city.
/// - categories: An array of `TournamentCategory` to filter by.
/// - levels: An array of `TournamentLevel` to filter by.
/// - lat: Optional latitude for precise location search.
/// - lng: Optional longitude for precise location search.
/// - ages: An array of `FederalTournamentAge` to filter by.
/// - types: An array of `FederalTournamentType` to filter by.
/// - nationalCup: A boolean indicating if national cup tournaments should be included.
/// - Returns: An array of `HttpCommand` objects, containing the `FederalTournament` data.
/// - Throws: An error if the network request fails or decoding the response is unsuccessful.
func getAllFederalTournaments(
sortingOption: String,
page: Int,
startDate: Date,
endDate: Date,
city: String,
distance: Double,
categories: [TournamentCategory],
levels: [TournamentLevel],
lat: String?,
lng: String?,
ages: [FederalTournamentAge],
types: [FederalTournamentType],
nationalCup: Bool
) async throws -> TournamentsAPIResponse {
let service = try StoreCenter.main.service()
// Construct query parameters for your backend API
var queryItems: [URLQueryItem] = [
URLQueryItem(name: "sort", value: sortingOption),
URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "start_date", value: startDate.twoDigitsYearFormatted),
URLQueryItem(name: "end_date", value: endDate.twoDigitsYearFormatted),
URLQueryItem(name: "city", value: city),
URLQueryItem(name: "distance", value: String(Int(distance))),
URLQueryItem(name: "national_cup", value: nationalCup ? "true" : "false")
]
if let lat = lat, !lat.isEmpty {
queryItems.append(URLQueryItem(name: "lat", value: lat))
}
if let lng = lng, !lng.isEmpty {
queryItems.append(URLQueryItem(name: "lng", value: lng))
}
// Add array parameters (assuming your backend can handle comma-separated or multiple query params)
if !categories.isEmpty {
queryItems.append(URLQueryItem(name: "categories", value: categories.map { String($0.rawValue) }.joined(separator: ",")))
}
if !levels.isEmpty {
queryItems.append(URLQueryItem(name: "levels", value: levels.map { String($0.rawValue) }.joined(separator: ",")))
}
if !ages.isEmpty {
queryItems.append(URLQueryItem(name: "ages", value: ages.map { String($0.rawValue) }.joined(separator: ",")))
}
if !types.isEmpty {
queryItems.append(URLQueryItem(name: "types", value: types.map { $0.rawValue }.joined(separator: ",")))
}
// Build the URL with query parameters
var urlComponents = URLComponents()
urlComponents.queryItems = queryItems
let queryString = urlComponents.query ?? ""
// The servicePath now points to your backend's endpoint for all tournaments: 'fft/all-tournaments/'
let urlRequest = try service._baseRequest(servicePath: "fft/all-tournaments?\(queryString)", method: .get, requiresToken: true)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
print(urlRequest.url?.absoluteString ?? "No URL")
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard !data.isEmpty else {
throw NetworkManagerError.noDataReceived
}
do {
// Your backend should return a direct array of FederalTournament
let federalTournaments = try JSONDecoder().decode(TournamentsAPIResponse.self, from: data)
return federalTournaments
} catch {
print("Decoding error for FederalTournament array in getAllFederalTournaments: \(error)")
throw NetworkManagerError.apiError("Failed to decode FederalTournament array: \(error.localizedDescription)")
}
}
/// Fetches umpire contact data for a given tournament ID.
/// This function now calls your backend, which performs the HTML scraping.
/// The return type is maintained for signature compatibility, mapping `UmpireContactInfo` to a tuple.
/// - Parameter idTournament: The ID of the tournament.
/// - Returns: A tuple `(name: String?, email: String?, phone: String?)` containing the umpire's contact info.
/// - Throws: An error if the network request fails or decoding the response is unsuccessful.
func getUmpireData(idTournament: String) async throws -> (name: String?, email: String?, phone: String?) {
let service = try StoreCenter.main.service()
// The servicePath now points to your backend's endpoint for umpire data: 'fft/umpire/{tournament_id}/'
let servicePath = "fft/umpire/\(idTournament)/"
let urlRequest = try service._baseRequest(servicePath: servicePath, method: .get, requiresToken: false)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard !data.isEmpty else {
throw NetworkManagerError.noDataReceived
}
do {
let umpireInfo = try JSONDecoder().decode(UmpireContactInfo.self, from: data)
// Map the decoded struct to the tuple required by the legacy signature
print(umpireInfo)
return (name: umpireInfo.name, email: umpireInfo.email, phone: umpireInfo.phone)
} catch {
print("Decoding error for UmpireContactInfo: \(error)")
throw NetworkManagerError.apiError("Failed to decode UmpireContactInfo: \(error.localizedDescription)")
}
}
/// Fetches umpire contact data for multiple tournament IDs.
/// This function calls your backend endpoint that handles multiple tournament IDs via query parameters.
/// - Parameter tournamentIds: An array of tournament ID strings.
/// - Returns: A dictionary mapping tournament IDs to tuples `(name: String?, email: String?, phone: String?)` containing the umpire's contact info.
/// - Throws: An error if the network request fails or decoding the response is unsuccessful.
func getUmpiresData(tournamentIds: [String]) async throws -> [String: (name: String?, email: String?, phone: String?)] {
let service = try StoreCenter.main.service()
// Validate input
guard !tournamentIds.isEmpty else {
throw NetworkManagerError.apiError("Tournament IDs array cannot be empty")
}
// Create the base service path
let basePath = "fft/umpires/"
// Build query parameters - join tournament IDs with commas
let tournamentIdsParam = tournamentIds.joined(separator: ",")
let queryItems = [URLQueryItem(name: "tournament_ids", value: tournamentIdsParam)]
// Create the URL with query parameters
var urlComponents = URLComponents()
urlComponents.queryItems = queryItems
let servicePath = basePath + (urlComponents.url?.query.map { "?\($0)" } ?? "")
let urlRequest = try service._baseRequest(servicePath: servicePath, method: .get, requiresToken: false)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard !data.isEmpty else {
throw NetworkManagerError.noDataReceived
}
// Check for HTTP errors
guard httpResponse.statusCode == 200 else {
if let errorData = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let message = errorData["message"] as? String {
throw NetworkManagerError.apiError("Server error: \(message)")
}
throw NetworkManagerError.apiError("HTTP error: \(httpResponse.statusCode)")
}
do {
let umpireResponse = try JSONDecoder().decode(UmpireDataResponse.self, from: data)
// Convert the results to the expected return format
var resultDict: [String: (name: String?, email: String?, phone: String?)] = [:]
for (tournamentId, umpireInfo) in umpireResponse.results {
resultDict[tournamentId] = (name: umpireInfo.name, email: umpireInfo.email, phone: umpireInfo.phone)
}
print("Umpire data fetched for \(resultDict.count) tournaments")
return resultDict
} catch {
print("Decoding error for UmpireDataResponse: \(error)")
throw NetworkManagerError.apiError("Failed to decode UmpireDataResponse: \(error.localizedDescription)")
}
}
}

@ -67,6 +67,7 @@ class NetworkFederalService {
func federalClubs(country: String = "fr", city: String, radius: Double, location: CLLocation? = nil) async throws -> FederalClubResponse {
return try await FederalDataService.shared.federalClubs(country: country, city: city, radius: radius, location: location)
/*
{
"geocoding[country]": "fr",
@ -114,211 +115,11 @@ class NetworkFederalService {
func getClubFederalTournaments(page: Int, tournaments: [FederalTournament], club: String, codeClub: String, startDate: Date? = nil, endDate: Date? = nil) async throws -> [FederalTournament] {
if formId.isEmpty {
do {
try await getNewBuildForm()
} catch {
print("getClubFederalTournaments", error)
}
}
var dateComponent = ""
if let startDate, let endDate {
dateComponent = "&date[start]=\(startDate.twoDigitsYearFormatted)&date[end]=\(endDate.endOfMonth.twoDigitsYearFormatted)"
} else if let startDate {
dateComponent = "&date[start]=\(startDate.twoDigitsYearFormatted)&date[end]=\(Calendar.current.date(byAdding: .month, value: 3, to: startDate)!.endOfMonth.twoDigitsYearFormatted)"
}
let parameters = """
recherche_type=club&club[autocomplete][value_container][value_field]=\(codeClub.replaceCharactersFromSet(characterSet: .whitespaces))&club[autocomplete][value_container][label_field]=\(club.replaceCharactersFromSet(characterSet: .whitespaces, replacementString: "+"))&pratique=PADEL\(dateComponent)&page=\(page)&sort=dateDebut+asc&form_build_id=\(formId)&form_id=recherche_tournois_form&_triggering_element_name=submit_page&_triggering_element_value=Submit+page
"""
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://tenup.fft.fr/system/ajax")!,timeoutInterval: Double.infinity)
request.addValue("application/json, text/javascript, */*; q=0.01", forHTTPHeaderField: "Accept")
request.addValue("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", forHTTPHeaderField: "Accept-Language")
request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding")
request.addValue("application/x-www-form-urlencoded; charset=UTF-8", forHTTPHeaderField: "Content-Type")
request.addValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
request.addValue("https://tenup.fft.fr", forHTTPHeaderField: "Origin")
request.addValue("keep-alive", forHTTPHeaderField: "Connection")
request.addValue("https://tenup.fft.fr/recherche/tournois", forHTTPHeaderField: "Referer")
request.addValue("empty", forHTTPHeaderField: "Sec-Fetch-Dest")
request.addValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode")
request.addValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site")
request.httpMethod = "POST"
request.httpBody = postData
let commands : [HttpCommand] = try await runTenupTask(request: request)
if commands.anySatisfy({ $0.command == "alert" }) {
throw NetworkManagerError.maintenance
}
let resultCommand = commands.first(where: { $0.results != nil })
if let gatheredTournaments = resultCommand?.results?.items {
var finalTournaments = tournaments + gatheredTournaments
if let count = resultCommand?.results?.nb_results {
if finalTournaments.count < count {
let newTournaments = try await getClubFederalTournaments(page: page+1, tournaments: finalTournaments, club: club, codeClub: codeClub)
finalTournaments = finalTournaments + newTournaments
}
}
return finalTournaments
}
// do {
// } catch {
// print("getClubFederalTournaments", error)
// }
//
return []
}
func getNewBuildForm() async throws {
var request = URLRequest(url: URL(string: "https://tenup.fft.fr/recherche/tournois")!,timeoutInterval: Double.infinity)
request.addValue("application/json, text/javascript, */*; q=0.01", forHTTPHeaderField: "Accept")
request.addValue("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", forHTTPHeaderField: "Accept-Language")
request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding")
request.addValue("application/x-www-form-urlencoded; charset=UTF-8", forHTTPHeaderField: "Content-Type")
request.addValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
request.addValue("https://tenup.fft.fr", forHTTPHeaderField: "Origin")
request.addValue("keep-alive", forHTTPHeaderField: "Connection")
request.addValue("https://tenup.fft.fr/recherche/tournois", forHTTPHeaderField: "Referer")
request.addValue("empty", forHTTPHeaderField: "Sec-Fetch-Dest")
request.addValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode")
request.addValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site")
request.addValue("trailers", forHTTPHeaderField: "TE")
request.httpMethod = "GET"
let task = try await URLSession.shared.data(for: request)
if let stringData = String(data: task.0, encoding: .utf8) {
let stringDataFolded = stringData.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines)
let prefix = "form_build_id\"value=\"form-"
var finalData = ""
if let lab = stringDataFolded.matches(of: try! Regex("\(prefix)")).last {
finalData = String(stringDataFolded[lab.range.upperBound...])
}
let suffix = "\"/><inputtype=\"hidden\"name=\"form_id\"value=\"recherche_tournois_form"
if let suff = finalData.firstMatch(of: try! Regex("\(suffix)")) {
finalData = String(finalData[..<suff.range.lowerBound])
}
print(finalData)
formId = "form-\(finalData)"
} else {
print("no data found in html")
}
}
func getAllFederalTournaments(sortingOption: String, page: Int, startDate: Date, endDate: Date, city: String, distance: Double, categories: [TournamentCategory], levels: [TournamentLevel], lat: String?, lng: String?, ages: [FederalTournamentAge], types: [FederalTournamentType], nationalCup: Bool) async throws -> [HttpCommand] {
var cityParameter = ""
var searchType = "ligue"
if city.trimmed.isEmpty == false {
searchType = "ville"
cityParameter = city
}
var levelsParameter = ""
if levels.isEmpty == false {
levelsParameter = levels.map { "categorie_tournoi[\($0.searchRawValue())]=\($0.searchRawValue())" }.joined(separator: "&") + "&"
}
var categoriesParameter = ""
if categories.isEmpty == false {
categoriesParameter = categories.map { "epreuve[\($0.requestLabel)]=\($0.requestLabel)" }.joined(separator: "&") + "&"
}
var agesParameter = ""
if ages.isEmpty == false {
agesParameter = ages.map { "categorie_age[\($0.rawValue)]=\($0.rawValue)" }.joined(separator: "&") + "&"
}
var typesParameter = ""
if types.isEmpty == false {
typesParameter = types.map { "type[\($0.rawValue.capitalized)]=\($0.rawValue.capitalized)" }.joined(separator: "&") + "&"
}
var npc = ""
if nationalCup {
npc = "&tournoi_npc=1"
}
let parameters = """
recherche_type=\(searchType)&ville%5Bautocomplete%5D%5Bcountry%5D=fr&ville%5Bautocomplete%5D%5Btextfield%5D=&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Bvalue_field%5D=\(cityParameter)&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blabel_field%5D=\(cityParameter)&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blat_field%5D=\(lat ?? "")&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blng_field%5D=\(lng ?? "")&ville%5Bdistance%5D%5Bvalue_field%5D=\(Int(distance))&club%5Bautocomplete%5D%5Btextfield%5D=&club%5Bautocomplete%5D%5Bvalue_container%5D%5Bvalue_field%5D=&club%5Bautocomplete%5D%5Bvalue_container%5D%5Blabel_field%5D=&pratique=PADEL&date%5Bstart%5D=\(startDate.twoDigitsYearFormatted)&date%5Bend%5D=\(endDate.twoDigitsYearFormatted)&\(categoriesParameter)\(levelsParameter)\(agesParameter)\(typesParameter)\(npc)&page=\(page)&sort=\(sortingOption)&form_build_id=\(formId)&form_id=recherche_tournois_form&_triggering_element_name=submit_page&_triggering_element_value=Submit+page
"""
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://tenup.fft.fr/system/ajax")!,timeoutInterval: Double.infinity)
request.addValue("application/json, text/javascript, */*; q=0.01", forHTTPHeaderField: "Accept")
request.addValue("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", forHTTPHeaderField: "Accept-Language")
request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding")
request.addValue("application/x-www-form-urlencoded; charset=UTF-8", forHTTPHeaderField: "Content-Type")
request.addValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
request.addValue("https://tenup.fft.fr", forHTTPHeaderField: "Origin")
request.addValue("keep-alive", forHTTPHeaderField: "Connection")
request.addValue("https://tenup.fft.fr/recherche/tournois", forHTTPHeaderField: "Referer")
request.addValue("empty", forHTTPHeaderField: "Sec-Fetch-Dest")
request.addValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode")
request.addValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site")
request.httpMethod = "POST"
request.httpBody = postData
return try await runTenupTask(request: request)
return try await FederalDataService.shared.getClubFederalTournaments(page: page, tournaments: tournaments, club: club, codeClub: codeClub, startDate: startDate, endDate: endDate).tournaments
}
func getUmpireData(idTournament: String) async throws -> (name: String?, email: String?, phone: String?) {
guard let url = URL(string: "https://tenup.fft.fr/tournoi/\(idTournament)") else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
guard let htmlString = String(data: data, encoding: .utf8) else {
throw URLError(.cannotDecodeContentData)
}
let namePattern = "tournoi-detail-page-inscription-responsable-title\">\\s*([^<]+)\\s*<"
let nameRegex = try? NSRegularExpression(pattern: namePattern)
let nameMatch = nameRegex?.firstMatch(in: htmlString, range: NSRange(htmlString.startIndex..., in: htmlString))
let name = nameMatch.flatMap { match in
Range(match.range(at: 1), in: htmlString)
}.map { range in
String(htmlString[range]).trimmingCharacters(in: .whitespacesAndNewlines)
}
// Extract email using regex
let emailPattern = "mailto:([^\"]+)\""
let emailRegex = try? NSRegularExpression(pattern: emailPattern)
let emailMatch = emailRegex?.firstMatch(in: htmlString, range: NSRange(htmlString.startIndex..., in: htmlString))
let email = emailMatch.flatMap { match in
Range(match.range(at: 1), in: htmlString)
}.map { range in
String(htmlString[range])
}
let pattern = "<div class=\"details-bloc\">\\s*(\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2})\\s*</div>"
var phoneNumber: String? = nil
// Look for the specific div and its content
if let range = htmlString.range(of: pattern, options: [.regularExpression, .caseInsensitive]) {
let match = String(htmlString[range])
let phonePattern = "\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2}"
if let phoneRange = match.range(of: phonePattern, options: .regularExpression) {
phoneNumber = String(match[phoneRange])
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
return (name, email, phoneNumber)
return try await FederalDataService.shared.getUmpireData(idTournament: idTournament)
}
}

@ -0,0 +1,59 @@
import Foundation
func areFrenchPhoneNumbersSimilar(_ phoneNumber1: String?, _ phoneNumber2: String?) -> Bool {
if phoneNumber1?.canonicalVersion == phoneNumber2?.canonicalVersion {
return true
}
// Helper function to normalize a phone number, now returning an optional String
func normalizePhoneNumber(_ numberString: String?) -> String? {
// 1. Safely unwrap the input string. If it's nil or empty, return nil immediately.
guard let numberString = numberString, !numberString.isEmpty else {
return nil
}
// 2. Remove all non-digit characters
let digitsOnly = numberString.filter(\.isNumber)
// If after filtering, there are no digits, return nil.
guard !digitsOnly.isEmpty else {
return nil
}
// 3. Handle French specific prefixes and extract the relevant part
// We need at least 9 digits to get a meaningful 8-digit comparison from the end
if digitsOnly.count >= 9 {
if digitsOnly.hasPrefix("0") {
return String(digitsOnly.suffix(9))
} else if digitsOnly.hasPrefix("33") {
// Ensure there are enough digits after dropping "33"
if digitsOnly.count >= 11 { // "33" + 9 digits = 11
return String(digitsOnly.dropFirst(2).suffix(9))
} else {
return nil // Not enough digits after dropping "33"
}
} else if digitsOnly.count == 9 { // Case like 612341234
return digitsOnly
} else { // More digits but no 0 or 33 prefix, take the last 9
return String(digitsOnly.suffix(9))
}
}
return nil // If it doesn't fit the expected patterns or is too short
}
// Normalize both phone numbers. If either results in nil, we can't compare.
guard let normalizedNumber1 = normalizePhoneNumber(phoneNumber1),
let normalizedNumber2 = normalizePhoneNumber(phoneNumber2) else {
return false
}
// Ensure both normalized numbers have at least 8 digits before comparing suffixes
guard normalizedNumber1.count >= 8 && normalizedNumber2.count >= 8 else {
return false // One or both numbers are too short to have 8 comparable digits
}
// Compare the last 8 digits
return normalizedNumber1.suffix(8) == normalizedNumber2.suffix(8)
}

@ -176,7 +176,7 @@ class FederalDataViewModel {
let collector = TournamentCollector()
try await clubs.filter { $0.code != nil }.concurrentForEach { club in
let newTournaments = try await NetworkFederalService.shared.getClubFederalTournaments(
let newTournaments = try await FederalDataService.shared.getClubFederalTournaments(
page: 0,
tournaments: [],
club: club.name,
@ -186,7 +186,7 @@ class FederalDataViewModel {
)
// Safely add to collector
await collector.add(tournaments: newTournaments)
await collector.add(tournaments: newTournaments.tournaments)
}
// Get all collected tournaments

@ -69,6 +69,13 @@ struct MenuWarningView: View {
Label("Appeler", systemImage: "phone")
Text(number)
}
if let contactPhoneNumber = player.contactPhoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(contactPhoneNumber)") {
Link(destination: url) {
Label("Appeler", systemImage: "phone")
Text(contactPhoneNumber)
}
}
} else {
Menu {
ForEach(players) { player in
@ -78,6 +85,12 @@ struct MenuWarningView: View {
Text(number)
}
}
if let number = player.contactPhoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label(player.playerLabel(.short), systemImage: "phone")
Text(number)
}
}
}
} label: {
Text("Appeler un joueur")
@ -93,12 +106,13 @@ struct MenuWarningView: View {
}
fileprivate func _contactByMessage(players: [PlayerRegistration], privateMode: Bool) {
self.savedContactType = .message(date: date, recipients: players.compactMap({ $0.phoneNumber }), body: message, tournamentBuild: nil)
self.savedContactType = .message(date: date, recipients: players.flatMap({ [$0.phoneNumber, $0.contactPhoneNumber].compacted() }), body: message, tournamentBuild: nil)
self._tryToContact()
}
fileprivate func _contactByMail(players: [PlayerRegistration], privateMode: Bool) {
self.savedContactType = .mail(date: date, recipients: privateMode ? _getUmpireMail() : players.compactMap({ $0.email }), bccRecipients: privateMode ? players.compactMap({ $0.email }) : nil, body: message, subject: subject, tournamentBuild: nil)
let mails = players.flatMap({ [$0.email, $0.contactEmail].compacted() })
self.savedContactType = .mail(date: date, recipients: privateMode ? _getUmpireMail() : mails, bccRecipients: privateMode ? mails : nil, body: message, subject: subject, tournamentBuild: nil)
self._tryToContact()
}

@ -18,6 +18,13 @@ struct EventSettingsView: View {
@State private var eventStartDate: Date
@FocusState private var focusedField: Tournament.CodingKeys?
var visibleOnPadelClub: Binding<Bool> {
Binding {
event.confirmedTournaments().allSatisfy({ $0.isPrivate == false })
} set: { _ in
}
}
func eventLinksPasteData() -> String {
let tournaments = event.tournaments
var link = [String]()
@ -105,6 +112,15 @@ struct EventSettingsView: View {
}
_saveAllTournaments()
}
}
}
Section {
Toggle(isOn: visibleOnPadelClub) {
Text("Visible sur Padel Club")
}
.onChange(of: visibleOnPadelClub.wrappedValue) { oldValue, newValue in
_saveAllTournaments()
}
}

@ -99,8 +99,7 @@ struct EventView: View {
case .club(let event):
EventClubSettingsView(event: event)
case .eventPlanning:
let allMatches = event.tournaments.flatMap { $0.allMatches() }
PlanningView(matches: allMatches, selectedScheduleDestination: .constant(nil))
PlanningView(matches: [], selectedScheduleDestination: .constant(nil), event: event)
.environment(\.matchViewStyle, .feedStyle)
case .links:
EventLinksView(event: event)

@ -55,6 +55,18 @@ struct TournamentConfigurationView: View {
} label: {
Text("Équipes souhaitées")
}
let minimumNumberOfTeams = tournament.minimumNumberOfTeams()
if minimumNumberOfTeams > 0 {
LabeledContent {
Text(minimumNumberOfTeams.formatted())
} label: {
Text("Minimum pour homologation")
}
}
}
}
//

@ -414,18 +414,25 @@ struct FederalClubResponse: Codable {
}
}
enum Pratique: String, Codable {
case beach = "BEACH"
case padel = "PADEL"
case tennis = "TENNIS"
case pickle = "PICKLE"
}
//enum Pratique: String, Codable {
// case beach = "BEACH"
// case padel = "PADEL"
// case tennis = "TENNIS"
// case pickle = "PICKLE"
//
// // Additional cases for the combined values
// case tennisPadel = "tennis-padel"
// case tennisPicklePadel = "tennis-pickle-padel"
// case tennisPadelBeach = "tennis-padel-beach"
// case padelOnly = "padel" // lowercase padel
//}
//
// MARK: - ClubMarker
struct ClubMarker: Codable, Hashable, Identifiable {
let nom, clubID, ville, distance: String
let terrainPratiqueLibelle: String
let pratiques: [Pratique]
let pratiques: [String]
let lat, lng: Double
// Method to get the number of courts for a specific sport
@ -449,7 +456,7 @@ struct ClubMarker: Codable, Hashable, Identifiable {
return courts
}
}
} else if pratiques.count == 1 && pratiques.first?.rawValue.lowercased() == sport.lowercased() {
} else if pratiques.count == 1 && pratiques.first?.lowercased() == sport.lowercased() {
// Handle cases where only the number of courts is provided (e.g., "2 terrains")
if let courtsNumber = trimmedComponent.split(separator: " ").first,
let courts = Int(courtsNumber) {

@ -58,9 +58,9 @@ struct EditSharingView: View {
messageData.append(message)
guard
let labelOne = match.team(.one)?.teamLabelRanked(
let labelOne = match.team(.one)?.teamLabelRanked(displayStyle: .title,
displayRank: displayRank, displayTeamName: displayTeamName),
let labelTwo = match.team(.two)?.teamLabelRanked(
let labelTwo = match.team(.two)?.teamLabelRanked(displayStyle: .title,
displayRank: displayRank, displayTeamName: displayTeamName)
else {
return messageData.joined(separator: "\n")

@ -314,6 +314,7 @@ struct ActivityView: View {
NavigationStack {
TournamentLookUpView()
.environment(federalDataViewModel)
.environment(navigation)
}
}
.sheet(item: $newTournament) { tournament in
@ -425,7 +426,11 @@ struct ActivityView: View {
Task {
do {
let clubs : [Club] = dataStore.user.clubsObjects()
try await federalDataViewModel.gatherTournaments(clubs: clubs.filter { $0.code != nil }, startDate: .now.startOfMonth)
try await federalDataViewModel.gatherTournaments(
clubs: clubs.filter { $0.code != nil },
startDate: .now.startOfCurrentMonth,
endDate: .now.startOfCurrentMonth.addingMonths(4)
)
} catch {
self.error = error
}

@ -333,6 +333,26 @@ struct EventListView: View {
} label: {
Text("Informations de contact Juge-Arbitre")
}
Divider()
Menu {
Button {
Task {
await pcTournaments.concurrentForEach { tournament in
if tournament.hasEnded() == false {
tournament.endDate = Date()
}
}
await MainActor.run {
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
}
}
} label: {
Text("Terminer les tournois encore ouverts")
}
} label: {
Text("Options avancées")
}
}
private func _nextMonths() -> [Date] {

@ -9,10 +9,12 @@ import SwiftUI
import CoreLocation
import CoreLocationUI
import PadelClubData
import LeStorage
struct TournamentLookUpView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
@Environment(NavigationViewModel.self) var navigationViewModel: NavigationViewModel
@StateObject var locationManager = LocationManager()
@Environment(\.dismiss) private var dismiss
@FocusState private var isFocused: Bool
@ -27,11 +29,20 @@ struct TournamentLookUpView: View {
@State private var presentAlert: Bool = false
@State private var confirmSearch: Bool = false
@State private var locationRequested = false
@State private var apiError: StoreError?
var tournaments: [FederalTournament] {
federalDataViewModel.searchedFederalTournaments
}
var presentApiError: Binding<Bool> {
Binding {
apiError != nil
} set: { value in
}
}
var showLastError: Binding<Bool> {
Binding {
locationManager.lastError != nil
@ -70,6 +81,26 @@ struct TournamentLookUpView: View {
secondaryButton: .cancel()
)
}
.alert(isPresented: presentApiError, error: apiError, actions: { storeError in
switch storeError {
case .missingUsername:
Button("Créer un compte ou se connecter") {
dismiss()
navigationViewModel.selectedTab = .umpire
}
default:
Button("D'accord") {
apiError = nil
}
}
}, message: { storeError in
switch storeError {
case .missingUsername:
Text("Un compte est requis pour utiliser ce service de Padel Club, veuillez créer un compte ou vous connecter.")
default:
Text("Une erreur est survenue, veuillez réessayer plus tard.")
}
})
.alert("Attention", isPresented: $presentAlert, actions: {
Button {
presentAlert = false
@ -79,7 +110,10 @@ struct TournamentLookUpView: View {
Task {
await getNewPage()
searching = false
dismiss()
if apiError == nil {
dismiss()
}
}
} label: {
Label("Tout voir", systemImage: "arrow.down.circle")
@ -187,13 +221,26 @@ struct TournamentLookUpView: View {
private func _gatherNumbers() {
Task {
print("Doing.....")
for i in 0..<tournaments.count {
print(i, "/", tournaments.count)
let phone = try? await NetworkFederalService.shared.getUmpireData(idTournament: tournaments[i].id).phone
federalDataViewModel.searchedFederalTournaments[i].updateJapPhoneNumber(phone: phone)
print(federalDataViewModel.searchedFederalTournaments[i].japMessage)
await withTaskGroup(of: (Int, String?).self) { group in
for i in 0..<tournaments.count {
let tournamentID = tournaments[i].id
let index = i // Capture index for use in the child task
group.addTask {
print("Starting task for tournament \(index) / \(self.tournaments.count)")
let phone = try? await NetworkFederalService.shared.getUmpireData(idTournament: tournamentID).phone
return (index, phone) // Return the index along with the phone number
}
}
// Process results as they complete
for await (index, phone) in group {
var tournamentData = federalDataViewModel.searchedFederalTournaments[index] // Get a mutable copy
tournamentData.updateJapPhoneNumber(phone: phone) // Mutate the copy
federalDataViewModel.searchedFederalTournaments[index] = tournamentData // Assign back
}
}
print(".....Done")
}
}
@ -219,7 +266,7 @@ struct TournamentLookUpView: View {
searching = false
if tournaments.isEmpty == false && tournaments.count < total && total >= 200 && requestedToGetAllPages == false {
presentAlert = true
} else {
} else if apiError == nil {
dismiss()
}
}
@ -251,57 +298,41 @@ struct TournamentLookUpView: View {
func getNewPage() async {
do {
if NetworkFederalService.shared.formId.isEmpty {
await getNewBuildForm()
} else {
let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: dataStore.appSettings.sortingOption, page: page, startDate: dataStore.appSettings.startDate, endDate: dataStore.appSettings.endDate, city: dataStore.appSettings.city, distance: dataStore.appSettings.distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: dataStore.appSettings.nationalCup)
if commands.anySatisfy({ $0.command == "alert" }) {
federalDataViewModel.lastError = .maintenance
}
let resultCommand = commands.first(where: { $0.results != nil })
if let newTournaments = resultCommand?.results?.items {
newTournaments.forEach { ft in
let commands = try await FederalDataService.shared.getAllFederalTournaments(sortingOption: dataStore.appSettings.sortingOption, page: page, startDate: dataStore.appSettings.startDate, endDate: dataStore.appSettings.endDate, city: dataStore.appSettings.city, distance: dataStore.appSettings.distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: dataStore.appSettings.nationalCup)
if commands.success == false {
federalDataViewModel.lastError = .maintenance
}
commands.tournaments.forEach { ft in
// let isValid = ft.tournaments.anySatisfy({ build in
// let ageValid = ages.isEmpty ? true : ages.contains(build.age)
// let levelValid = levels.isEmpty ? true : levels.contains(build.level)
// let categoryValid = categories.isEmpty ? true : categories.contains(build.category)
// return ageValid && levelValid && categoryValid
// })
if tournaments.contains(where: { $0.id == ft.id }) == false {
federalDataViewModel.searchedFederalTournaments.append(ft)
}
}
if tournaments.contains(where: { $0.id == ft.id }) == false {
federalDataViewModel.searchedFederalTournaments.append(ft)
}
if let count = resultCommand?.results?.nb_results {
print("count", count, total, tournaments.count, page)
total = count
if tournaments.count < count && page < total / 30 {
if total < 200 || requestedToGetAllPages {
page += 1
await getNewPage()
}
} else {
print("finished")
}
} else {
print("total results not found")
}
let count = commands.totalResults
print("count", count, total, tournaments.count, page)
total = count
if tournaments.count < count && page < total / 30 {
if total < 200 || requestedToGetAllPages {
page += 1
await getNewPage()
}
} else {
print("finished")
}
} catch {
} catch let error as StoreError {
print("getNewPage", error)
await getNewBuildForm()
}
}
func getNewBuildForm() async {
do {
try await NetworkFederalService.shared.getNewBuildForm()
await getNewPage()
apiError = error
} catch {
print("getNewBuildForm", error)
print("getNewPage", error)
}
}

@ -275,6 +275,14 @@ struct PadelClubView: View {
}
}
struct RankingJSON: Decodable {
enum CodingKeys: String, CodingKey {
case joueurs
}
let joueurs: [FederalPlayer]
}
private func _exportCsv() async {
for fileURL in SourceFileManager.shared.jsonFiles() {
let decoder = JSONDecoder()
@ -282,18 +290,16 @@ struct PadelClubView: View {
do {
let data = try Data(contentsOf: fileURL)
let players = try decoder.decode([FederalPlayer].self, from: data)
let players = try decoder.decode(RankingJSON.self, from: data).joueurs
var anonymousPlayers = players.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }
let okPlayers = players.filter { $0.firstName.isEmpty == false && $0.lastName.isEmpty == false }
print("before anonymousPlayers.count", anonymousPlayers.count)
FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers)
print("after local anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count)
//FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers)
// print("after local anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count)
await fetchPlayersDataSequentially(for: &anonymousPlayers)
//await fetchPlayersDataSequentially(for: &anonymousPlayers)
print("after beach anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }
.count)
// print("after beach anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count)
SourceFileManager.shared.exportToCSV(players: players, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
SourceFileManager.shared.exportToCSV("anonymes", players: anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
} catch {

@ -22,9 +22,15 @@ struct PlanningView: View {
let updatePlannedDatesTip = UpdatePlannedDatesTip()
let allMatches: [Match]
let timeSlotMoveOptionTip = TimeSlotMoveOptionTip()
let event: Event?
init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>) {
self.allMatches = matches
init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>, event: Event? = nil) {
self.event = event
if let event {
self.allMatches = event.confirmedTournaments().flatMap { $0.allMatches() }
} else {
self.allMatches = matches
}
_selectedScheduleDestination = selectedScheduleDestination
}
@ -132,6 +138,24 @@ struct PlanningView: View {
} else {
ToolbarItemGroup(placement: .topBarTrailing) {
Menu {
if let event {
Menu {
Button {
event.confirmedTournaments().forEach { tournament in
tournament.removeAllDates()
}
} label: {
Text("Effacer tous les horaires")
}
Button {
_planEvent(event: event)
} label: {
Text("Planifier")
}
} label: {
Text("Planifier l'événement")
}
}
if notSlots == false {
CourtOptionsView(timeSlots: timeSlots, underlined: false)
Toggle(isOn: $enableMove) {
@ -226,11 +250,26 @@ struct PlanningView: View {
RowButtonView("Horaire intelligent") {
selectedScheduleDestination = nil
}
} else if let event {
RowButtonView("Planifier automatiquement") {
_planEvent(event: event)
}
}
}
}
}
}
private func _planEvent(event: Event) {
event.confirmedTournaments().forEach { tournament in
if let matchScheduler = tournament.matchScheduler() {
_ = matchScheduler.updateSchedule(tournament: tournament)
} else {
let matchScheduler = MatchScheduler(tournament: tournament.id, courtsAvailable: Set(tournament.courtsAvailable()))
_ = matchScheduler.updateSchedule(tournament: tournament)
}
}
}
struct BySlotView: View {
@Environment(\.filterOption) private var filterOption

@ -18,6 +18,12 @@ struct PlayerDetailView: View {
@State private var licenceId: String
@State private var phoneNumber: String
@State private var email: String
@State private var contactName: String
@State private var contactPhoneNumber: String
@State private var contactEmail: String
@FocusState var focusedField: PlayerRegistration.CodingKeys?
var tournamentStore: TournamentStore? {
@ -29,6 +35,9 @@ struct PlayerDetailView: View {
_licenceId = .init(wrappedValue: player.licenceId ?? "")
_email = .init(wrappedValue: player.email ?? "")
_phoneNumber = .init(wrappedValue: player.phoneNumber ?? "")
_contactName = .init(wrappedValue: player.contactName ?? "")
_contactEmail = .init(wrappedValue: player.contactEmail ?? "")
_contactPhoneNumber = .init(wrappedValue: player.contactPhoneNumber ?? "")
}
var body: some View {
@ -148,7 +157,7 @@ struct PlayerDetailView: View {
}
} else if player.isMalePlayer() == false && tournament.tournamentCategory == .men, let rank = player.rank {
Section {
let value = PlayerRegistration.addon(for: rank, manMax: maxMaleUnrankedValue, womanMax: tournament.femaleUnrankedValue ?? 0)
let value = tournament.addon(for: rank, manMax: maxMaleUnrankedValue, womanMax: tournament.femaleUnrankedValue ?? 0)
LabeledContent {
Text(value.formatted())
} label: {
@ -210,6 +219,18 @@ struct PlayerDetailView: View {
}
} label: {
Menu {
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: "") { if let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("\(number)", systemImage: "phone")
}
}
if let url = URL(string: "sms:\(number)") {
Link(destination: url) {
Label("\(number)", systemImage: "message")
}
}
Divider()
}
CopyPasteButtonView(pasteValue: player.phoneNumber)
PasteButtonView(text: $phoneNumber)
} label: {
@ -231,33 +252,103 @@ struct PlayerDetailView: View {
}
} label: {
Menu {
if let mail = player.email, let mailURL = URL(string: "mail:\(mail)") {
Link(destination: mailURL) {
Label(mail, systemImage: "mail")
}
Divider()
}
CopyPasteButtonView(pasteValue: player.email)
PasteButtonView(text: $email)
} label: {
Text("Email")
}
}
} header: {
Text("Information fédérale")
}
Section {
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: "") {
if let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("Appeler", systemImage: "phone")
LabeledContent {
TextField("Contact/tuteur", text: $contactName)
.focused($focusedField, equals: ._contactName)
.keyboardType(.alphabet)
.textContentType(nil)
.multilineTextAlignment(.trailing)
.autocorrectionDisabled()
.frame(maxWidth: .infinity)
.onSubmit(of: .text) {
player.contactName = contactName.prefixTrimmed(200)
_save()
}
}
if let url = URL(string: "sms:\(number)") {
Link(destination: url) {
Label("Message", systemImage: "message")
} label: {
Text("Contact/tuteur")
}
LabeledContent {
TextField("Téléphone contact", text: $contactPhoneNumber)
.focused($focusedField, equals: ._contactPhoneNumber)
.keyboardType(.namePhonePad)
.textContentType(nil)
.multilineTextAlignment(.trailing)
.autocorrectionDisabled()
.frame(maxWidth: .infinity)
.onSubmit(of: .text) {
player.contactPhoneNumber = contactPhoneNumber.prefixTrimmed(50)
_save()
}
} label: {
Menu {
if let number = player.contactPhoneNumber?.replacingOccurrences(of: " ", with: "") { if let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("\(number)", systemImage: "phone")
}
}
if let url = URL(string: "sms:\(number)") {
Link(destination: url) {
Label(number, systemImage: "message")
}
}
Divider()
}
CopyPasteButtonView(pasteValue: player.contactPhoneNumber)
PasteButtonView(text: $contactPhoneNumber)
} label: {
Text("Téléphone contact")
}
}
if let mail = player.email, let mailURL = URL(string: "mail:\(mail)") {
Link(destination: mailURL) {
Label("Mail", systemImage: "mail")
LabeledContent {
TextField("Email contact", text: $contactEmail)
.focused($focusedField, equals: ._contactEmail)
.keyboardType(.emailAddress)
.textContentType(nil)
.multilineTextAlignment(.trailing)
.autocorrectionDisabled()
.frame(maxWidth: .infinity)
.onSubmit(of: .text) {
player.contactEmail = contactEmail.prefixTrimmed(50)
_save()
}
} label: {
Menu {
if let mail = player.contactEmail, let mailURL = URL(string: "mail:\(mail)") {
Link(destination: mailURL) {
Label(mail, systemImage: "mail")
}
Divider()
}
CopyPasteButtonView(pasteValue: player.contactEmail)
PasteButtonView(text: $contactEmail)
} label: {
Text("Email contact")
}
}
} header: {
Text("Information de contact")
} footer: {
Text("Ces champs vous permettent de garder les informations de contacts avec le joueur s'ils sont différents de ceux renvoyées par la base fédérale. Cela permet également de garder le contact d'un parent s'il s'agit d'un tournoi enfant.")
}
// Section {

@ -268,7 +268,7 @@ struct EditingTeamView: View {
Text("Nom de l'équipe")
}
if tournament.tournamentLevel.coachingIsAuthorized {
if tournament.coachingIsAuthorized() {
CoachListView(team: team)
}

@ -64,6 +64,7 @@ enum FileImportCustomField: Int, Identifiable, CaseIterable {
struct FileImportView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) var navigationViewModel: NavigationViewModel
@Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) private var dismiss
@ -84,7 +85,16 @@ struct FileImportView: View {
@State private var presentFormatHelperView: Bool = false
@State private var validatedTournamentIds: Set<String> = Set()
@State private var chunkMode: ChunkMode = .byParameter
@State private var apiError: StoreError?
var presentApiError: Binding<Bool> {
Binding {
apiError != nil
} set: { value in
}
}
enum ChunkMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue }
case byParameter
@ -175,7 +185,11 @@ struct FileImportView: View {
if let fileContent {
do {
try await _startImport(fileContent: fileContent, allTournaments: false)
} catch let error as StoreError {
Logger.error(error)
apiError = error
} catch {
Logger.error(error)
errorMessage = error.localizedDescription
}
}
@ -199,7 +213,11 @@ struct FileImportView: View {
if let fileContent {
do {
try await _startImport(fileContent: fileContent, allTournaments: true)
} catch let error as StoreError {
Logger.error(error)
apiError = error
} catch {
Logger.error(error)
errorMessage = error.localizedDescription
}
}
@ -306,7 +324,11 @@ struct FileImportView: View {
if let fileContent {
do {
try await _startImport(fileContent: fileContent, allTournaments: false)
} catch let error as StoreError {
Logger.error(error)
apiError = error
} catch {
Logger.error(error)
errorMessage = error.localizedDescription
}
}
@ -381,6 +403,26 @@ struct FileImportView: View {
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.alert(isPresented: presentApiError, error: apiError, actions: { storeError in
switch storeError {
case .missingUsername:
Button("Créer un compte ou se connecter") {
dismiss()
navigationViewModel.selectedTab = .umpire
}
default:
Button("D'accord") {
apiError = nil
}
}
}, message: { storeError in
switch storeError {
case .missingUsername:
Text("Un compte est requis pour utiliser ce service de Padel Club, veuillez créer un compte ou vous connecter.")
default:
Text("Une erreur est survenue, veuillez réessayer plus tard.")
}
})
.sheet(isPresented: $presentFormatHelperView) {
NavigationStack {
List {
@ -434,6 +476,9 @@ struct FileImportView: View {
fileContent = try String(contentsOf: selectedFile)
}
selectedFile.stopAccessingSecurityScopedResource()
} catch let error as StoreError {
Logger.error(error)
apiError = error
} catch {
Logger.error(error)
errorMessage = error.localizedDescription
@ -453,6 +498,7 @@ struct FileImportView: View {
do {
fileContent = try String(contentsOf: url)
} catch {
Logger.error(error)
errorMessage = error.localizedDescription
}
}

@ -51,5 +51,16 @@ struct TournamentLevelPickerView: View {
Text(type.localizedLabel()).tag(type)
}
}
let minimumNumberOfTeams = tournament.minimumNumberOfTeams()
if minimumNumberOfTeams > 0 {
LabeledContent {
Text(minimumNumberOfTeams.formatted())
} label: {
Text("Minimum pour homologation")
}
}
}
}

@ -285,6 +285,7 @@ struct InscriptionManagerView: View {
}) {
NavigationStack {
FileImportView(defaultFileProvider: tournament.isAnimation() ? .custom : .frenchFederation)
.environment(navigation)
}
.tint(.master)
}

@ -28,7 +28,11 @@ struct PrintSettingsView: View {
Section {
// Toggle(isOn: $generator.displayHeads, label: {
// Text("Afficher les têtes de séries")
// })
// }
Toggle(isOn: $generator.displayPlannedDate, label: {
Text("Afficher la date plannifiée")
})
Toggle(isOn: $generator.displayTeamIndex, label: {
Text("Afficher le poids et le rang de l'équipe")
@ -46,11 +50,17 @@ struct PrintSettingsView: View {
Toggle(isOn: $generator.includeBracket, label: {
Text("Tableau")
})
.onChange(of: generator.includeBracket) { oldValue, newValue in
if newValue == false {
generator.includeLoserBracket = newValue
}
}
Toggle(isOn: $generator.includeLoserBracket, label: {
Text("Tableau des matchs de classements")
})
.disabled(generator.includeBracket == false)
if tournament.groupStages().isEmpty == false {
Toggle(isOn: $generator.includeGroupStage, label: {
Text("Poules")
@ -58,71 +68,69 @@ struct PrintSettingsView: View {
}
}
if generator.includeBracket {
Section {
Picker(selection: $generator.zoomLevel) {
Text("1 page").tag(nil as Optional<CGFloat>)
Text("50%").tag(2.0 as Optional<CGFloat>)
Text("100%").tag(1.0 as Optional<CGFloat>)
} label: {
Text("Zoom")
}
.onChange(of: generator.zoomLevel) {
if generator.zoomLevel == nil {
generator.landscape = false
}
}
if generator.zoomLevel != nil {
Toggle(isOn: $generator.landscape, label: {
Text("Format paysage")
})
}
HStack {
Text("Nombre de page A4 à imprimer")
Spacer()
Text(generator.estimatedPageCount.formatted())
Section {
Picker(selection: $generator.zoomLevel) {
Text("1 page").tag(nil as Optional<CGFloat>)
Text("50%").tag(2.0 as Optional<CGFloat>)
Text("100%").tag(1.0 as Optional<CGFloat>)
} label: {
Text("Zoom")
}
.onChange(of: generator.zoomLevel) {
if generator.zoomLevel == nil {
generator.landscape = false
}
} header: {
Text("Tableau principal")
}
if generating == false {
RowButtonView("Générer le PDF", systemImage: "printer") {
await MainActor.run() {
self.generating = true
}
generator.preparePDF { result in
switch result {
case .success(true):
if generator.includeGroupStage && generator.groupStageIsReady == false && tournament.groupStages().isEmpty == false {
self.prepareGroupStage = true
self.generationGroupStageId = UUID()
} else {
self.presentShareView = true
self.generating = false
}
case .success(false):
print("didn't save pdf")
break
case .failure(let error):
print(error)
break
if generator.zoomLevel != nil {
Toggle(isOn: $generator.landscape, label: {
Text("Format paysage")
})
}
HStack {
Text("Nombre de page A4 à imprimer")
Spacer()
Text(generator.estimatedPageCount.formatted())
}
} header: {
Text("Tableau principal")
}
if generating == false {
RowButtonView("Générer le PDF", systemImage: "printer") {
await MainActor.run() {
self.generating = true
}
generator.preparePDF { result in
switch result {
case .success(true):
if generator.includeGroupStage && generator.groupStageIsReady == false && tournament.groupStages().isEmpty == false {
self.prepareGroupStage = true
self.generationGroupStageId = UUID()
} else {
self.presentShareView = true
self.generating = false
}
case .success(false):
print("didn't save pdf")
break
case .failure(let error):
print(error)
break
}
self.prepareGroupStage = false
self.generationId = UUID()
}
.disabled(generator.includeBracket == false && generator.includeGroupStage == false && generator.includeLoserBracket == false)
} else {
LabeledContent {
ProgressView()
} label: {
Text("Préparation du PDF")
}
.id(generationId)
self.prepareGroupStage = false
self.generationId = UUID()
}
.disabled(generator.includeBracket == false && generator.includeGroupStage == false)
} else {
LabeledContent {
ProgressView()
} label: {
Text("Préparation du PDF")
}
.id(generationId)
}
Section {
@ -178,7 +186,7 @@ struct PrintSettingsView: View {
}
if let groupStage = tournament.groupStages().first {
ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore)) {
ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(options: generator.options)) {
Text("Poule")
}
}
@ -219,7 +227,7 @@ struct PrintSettingsView: View {
Group {
if prepareGroupStage {
ForEach(tournament.groupStages()) { groupStage in
WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore), loadStatusChanged: { loaded, error, webView in
WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(options: generator.options), loadStatusChanged: { loaded, error, webView in
if let error {
print("preparePDF", error)
} else if loaded == false {
@ -321,7 +329,7 @@ struct WebViewPreview: View {
ProgressView()
.onAppear {
if let groupStage {
html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore)
html = HtmlService.groupstage(groupStage: groupStage).html(options: generator.options)
} else if let round {
html = generator.generateLoserBracketHtml(upperRound: round)
} else {

@ -95,6 +95,12 @@ struct TableStructureView: View {
}
} label: {
Text("Nombre d'équipes")
let minimumNumberOfTeams = tournament.minimumNumberOfTeams()
if minimumNumberOfTeams > 0 {
Text("Minimum pour homologation : \(minimumNumberOfTeams)")
.foregroundStyle(.secondary)
}
}
LabeledContent {
StepperView(count: $groupStageCount, minimum: 0, maximum: maxGroupStages) {
@ -106,7 +112,9 @@ struct TableStructureView: View {
Text("Nombre de poules")
}
} footer: {
Text("Vous pourrez modifier la taille de vos poules de manière spécifique dans l'écran des poules.")
if groupStageCount > 0 {
Text("Vous pourrez modifier la taille de vos poules de manière spécifique dans l'écran des poules.")
}
}
if groupStageCount > 0 {

@ -310,7 +310,7 @@ final class ServerDataTests: XCTestCase {
return
}
let playerRegistration = PlayerRegistration(teamRegistration: teamRegistrationId, firstName: "juan", lastName: "lebron", licenceId: "123", rank: 11, paymentType: PlayerPaymentType.cash, sex: PlayerSexType.male, tournamentPlayed: 2, points: 33, clubName: "le club", ligueName: "la league", assimilation: "ass", phoneNumber: "123123", email: "email@email.com", birthdate: nil, computedRank: 222, source: PlayerRegistration.PlayerDataSource.frenchFederation, hasArrived: true, coach: false, captain: false, registeredOnline: false, timeToConfirm: nil, registrationStatus: PlayerRegistration.RegistrationStatus.waiting, paymentId: nil)
let playerRegistration = PlayerRegistration(teamRegistration: teamRegistrationId, firstName: "juan", lastName: "lebron", licenceId: "123", rank: 11, paymentType: PlayerPaymentType.cash, sex: PlayerSexType.male, tournamentPlayed: 2, points: 33, clubName: "le club", ligueName: "la league", assimilation: "ass", phoneNumber: "123123", email: "email@email.com", birthdate: nil, computedRank: 222, source: PlayerRegistration.PlayerDataSource.frenchFederation, hasArrived: true, coach: false, captain: false, registeredOnline: false, timeToConfirm: nil, registrationStatus: PlayerRegistration.RegistrationStatus.waiting, paymentId: nil, contactName: "coach juan", contactPhoneNumber: "4587654321", contactEmail: "juana@email.com")
playerRegistration.storeId = "123"
if let pr: PlayerRegistration = try await StoreCenter.main.service().post(playerRegistration) {
@ -343,6 +343,10 @@ final class ServerDataTests: XCTestCase {
assert(pr.timeToConfirm == playerRegistration.timeToConfirm)
assert(pr.registrationStatus == playerRegistration.registrationStatus)
assert(pr.paymentId == playerRegistration.paymentId)
assert(pr.contactName == playerRegistration.contactName)
assert(pr.contactEmail == playerRegistration.contactEmail)
assert(pr.contactPhoneNumber == playerRegistration.contactPhoneNumber)
} else {
XCTFail("missing data")
}

Loading…
Cancel
Save