diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 3a1585f..2ac6591 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -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 = ""; }; FF7DCD382CC330260041110C /* TeamRestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRestingView.swift; sourceTree = ""; }; FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSubscriptionView.swift; sourceTree = ""; }; + FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumbersUtils.swift; sourceTree = ""; }; FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = ""; }; FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; FF8E1CE52C006E0200184680 /* Alphabet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alphabet.swift; sourceTree = ""; }; @@ -1140,6 +1147,7 @@ FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLookUpView.swift; sourceTree = ""; }; FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = ""; }; FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; + FFD883782E1E63880004D7DD /* FederalDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalDataService.swift; sourceTree = ""; }; FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = ""; }; 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 = ""; }; @@ -1739,6 +1747,7 @@ FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */, FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */, FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */, + FFD883782E1E63880004D7DD /* FederalDataService.swift */, ); path = Network; sourceTree = ""; @@ -1935,6 +1944,7 @@ FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, FF1DC5582BAB767000FD8220 /* Tips.swift */, C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */, + FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */, ); path = Utils; sourceTree = ""; @@ -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 = ""; diff --git a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift index d65f340..f5c929d 100644 --- a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift +++ b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift @@ -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()) } } diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift index d5a6784..3573a3e 100644 --- a/PadelClub/Data/Federal/FederalPlayer.swift +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -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 diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index 0021712..cb214d4 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -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? diff --git a/PadelClub/Extensions/MonthData+Extensions.swift b/PadelClub/Extensions/MonthData+Extensions.swift index 0f6bbf6..13d8502 100644 --- a/PadelClub/Extensions/MonthData+Extensions.swift +++ b/PadelClub/Extensions/MonthData+Extensions.swift @@ -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) - } } diff --git a/PadelClub/Extensions/TeamRegistration+Extensions.swift b/PadelClub/Extensions/TeamRegistration+Extensions.swift index c3c2140..5cdfd84 100644 --- a/PadelClub/Extensions/TeamRegistration+Extensions.swift +++ b/PadelClub/Extensions/TeamRegistration+Extensions.swift @@ -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 diff --git a/PadelClub/Extensions/Tournament+Extensions.swift b/PadelClub/Extensions/Tournament+Extensions.swift index a928ded..65408aa 100644 --- a/PadelClub/Extensions/Tournament+Extensions.swift +++ b/PadelClub/Extensions/Tournament+Extensions.swift @@ -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 diff --git a/PadelClub/HTML Templates/match-template.html b/PadelClub/HTML Templates/match-template.html index bf0365d..1f6b761 100644 --- a/PadelClub/HTML Templates/match-template.html +++ b/PadelClub/HTML Templates/match-template.html @@ -3,6 +3,7 @@
{{matchDescriptionTop}}
  • +
    {{centerMatchText}}
  • diff --git a/PadelClub/HTML Templates/tournament-template.html b/PadelClub/HTML Templates/tournament-template.html index 03c77d2..72d6621 100644 --- a/PadelClub/HTML Templates/tournament-template.html +++ b/PadelClub/HTML Templates/tournament-template.html @@ -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 */ + } diff --git a/PadelClub/Utils/HtmlGenerator.swift b/PadelClub/Utils/HtmlGenerator.swift index 6ec685a..f91c736 100644 --- a/PadelClub/Utils/HtmlGenerator.swift +++ b/PadelClub/Utils/HtmlGenerator.swift @@ -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? { diff --git a/PadelClub/Utils/HtmlService.swift b/PadelClub/Utils/HtmlService.swift index da7e2f0..dcc92dc 100644 --- a/PadelClub/Utils/HtmlService.swift +++ b/PadelClub/Utils/HtmlService.swift @@ -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: #"
    {{teamIndex}}
    "#, 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..{{teamIndex}}
    "#, 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 = """
      @@ -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 } } diff --git a/PadelClub/Utils/Network/FederalDataService.swift b/PadelClub/Utils/Network/FederalDataService.swift new file mode 100644 index 0000000..9cf9bf0 --- /dev/null +++ b/PadelClub/Utils/Network/FederalDataService.swift @@ -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)") + } + } + +} diff --git a/PadelClub/Utils/Network/NetworkFederalService.swift b/PadelClub/Utils/Network/NetworkFederalService.swift index 66d013d..2d09be2 100644 --- a/PadelClub/Utils/Network/NetworkFederalService.swift +++ b/PadelClub/Utils/Network/NetworkFederalService.swift @@ -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 = "\"/> [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 = "
      \\s*(\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2})\\s*
      " - - 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) } } diff --git a/PadelClub/Utils/PhoneNumbersUtils.swift b/PadelClub/Utils/PhoneNumbersUtils.swift new file mode 100644 index 0000000..cd446ac --- /dev/null +++ b/PadelClub/Utils/PhoneNumbersUtils.swift @@ -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) +} diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index 9bc23a0..593c6c5 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -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 diff --git a/PadelClub/Views/Calling/Components/MenuWarningView.swift b/PadelClub/Views/Calling/Components/MenuWarningView.swift index 3f3bcc5..885b1cf 100644 --- a/PadelClub/Views/Calling/Components/MenuWarningView.swift +++ b/PadelClub/Views/Calling/Components/MenuWarningView.swift @@ -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() } diff --git a/PadelClub/Views/Cashier/Event/EventSettingsView.swift b/PadelClub/Views/Cashier/Event/EventSettingsView.swift index d75603a..76a8db2 100644 --- a/PadelClub/Views/Cashier/Event/EventSettingsView.swift +++ b/PadelClub/Views/Cashier/Event/EventSettingsView.swift @@ -18,6 +18,13 @@ struct EventSettingsView: View { @State private var eventStartDate: Date @FocusState private var focusedField: Tournament.CodingKeys? + var visibleOnPadelClub: Binding { + 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() } } diff --git a/PadelClub/Views/Cashier/Event/EventView.swift b/PadelClub/Views/Cashier/Event/EventView.swift index e04154a..23caf52 100644 --- a/PadelClub/Views/Cashier/Event/EventView.swift +++ b/PadelClub/Views/Cashier/Event/EventView.swift @@ -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) diff --git a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift index 9f5b111..b114a88 100644 --- a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift +++ b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift @@ -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") + } + + } + } } // diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index 72c880b..6c54e3a 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -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) { diff --git a/PadelClub/Views/Match/EditSharingView.swift b/PadelClub/Views/Match/EditSharingView.swift index 12a57e2..b8929f8 100644 --- a/PadelClub/Views/Match/EditSharingView.swift +++ b/PadelClub/Views/Match/EditSharingView.swift @@ -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") diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 0c6925a..cd0c005 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -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 } diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index 0e5689e..7668224 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -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] { diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index 5f9410f..d6417e7 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -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 { + Binding { + apiError != nil + } set: { value in + + } + } + var showLastError: Binding { 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..= 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) } } diff --git a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift index ef9a947..29f0145 100644 --- a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift +++ b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift @@ -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 { diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index 33137e2..f6f7a34 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -22,9 +22,15 @@ struct PlanningView: View { let updatePlannedDatesTip = UpdatePlannedDatesTip() let allMatches: [Match] let timeSlotMoveOptionTip = TimeSlotMoveOptionTip() + let event: Event? - init(matches: [Match], selectedScheduleDestination: Binding) { - self.allMatches = matches + init(matches: [Match], selectedScheduleDestination: Binding, 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 diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 8199432..3b1eaa8 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -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 { diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index d239148..6136e36 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -268,7 +268,7 @@ struct EditingTeamView: View { Text("Nom de l'équipe") } - if tournament.tournamentLevel.coachingIsAuthorized { + if tournament.coachingIsAuthorized() { CoachListView(team: team) } diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 70b82c2..0295501 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -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 = Set() @State private var chunkMode: ChunkMode = .byParameter - + @State private var apiError: StoreError? + + var presentApiError: Binding { + 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 } } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift index dc58592..2c21ffa 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift @@ -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") + } + + } } } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index cde3a24..89b091d 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -285,6 +285,7 @@ struct InscriptionManagerView: View { }) { NavigationStack { FileImportView(defaultFileProvider: tournament.isAnimation() ? .custom : .frenchFederation) + .environment(navigation) } .tint(.master) } diff --git a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift index 0693086..2cc6c92 100644 --- a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift @@ -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) - Text("50%").tag(2.0 as Optional) - Text("100%").tag(1.0 as Optional) - } 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) + Text("50%").tag(2.0 as Optional) + Text("100%").tag(1.0 as Optional) + } 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 { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 8d30e92..43805d5 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -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 { diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index a0729e0..98735cb 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -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") }