Merge branch 'main'

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

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

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

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

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

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

@ -39,6 +39,17 @@ extension TeamRegistration {
player.licenceId?.strippedLicense != nil player.licenceId?.strippedLicense != nil
{ {
player.registeredOnline = oldPlayer.registeredOnline 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.coach = oldPlayer.coach
player.tournamentPlayed = oldPlayer.tournamentPlayed player.tournamentPlayed = oldPlayer.tournamentPlayed
player.points = oldPlayer.points player.points = oldPlayer.points

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

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

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

@ -27,6 +27,7 @@ class HtmlGenerator: ObservableObject {
@Published var displayRank: Bool = false @Published var displayRank: Bool = false
@Published var displayTeamIndex: Bool = false @Published var displayTeamIndex: Bool = false
@Published var displayScore: Bool = false @Published var displayScore: Bool = false
@Published var displayPlannedDate: Bool = true
private var pdfDocument: PDFDocument = PDFDocument() private var pdfDocument: PDFDocument = PDFDocument()
private var rects: [CGRect] = [] private var rects: [CGRect] = []
@ -179,12 +180,16 @@ class HtmlGenerator: ObservableObject {
func generateHtml() -> String { func generateHtml() -> String {
//HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() //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 { func generateLoserBracketHtml(upperRound: Round) -> String {
//HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() //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? { var pdfURL: URL? {

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

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

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

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

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

@ -69,6 +69,13 @@ struct MenuWarningView: View {
Label("Appeler", systemImage: "phone") Label("Appeler", systemImage: "phone")
Text(number) 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 { } else {
Menu { Menu {
ForEach(players) { player in ForEach(players) { player in
@ -78,6 +85,12 @@ struct MenuWarningView: View {
Text(number) 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: { } label: {
Text("Appeler un joueur") Text("Appeler un joueur")
@ -93,12 +106,13 @@ struct MenuWarningView: View {
} }
fileprivate func _contactByMessage(players: [PlayerRegistration], privateMode: Bool) { 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() self._tryToContact()
} }
fileprivate func _contactByMail(players: [PlayerRegistration], privateMode: Bool) { 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() self._tryToContact()
} }

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

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

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

@ -414,18 +414,25 @@ struct FederalClubResponse: Codable {
} }
} }
enum Pratique: String, Codable { //enum Pratique: String, Codable {
case beach = "BEACH" // case beach = "BEACH"
case padel = "PADEL" // case padel = "PADEL"
case tennis = "TENNIS" // case tennis = "TENNIS"
case pickle = "PICKLE" // 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 // MARK: - ClubMarker
struct ClubMarker: Codable, Hashable, Identifiable { struct ClubMarker: Codable, Hashable, Identifiable {
let nom, clubID, ville, distance: String let nom, clubID, ville, distance: String
let terrainPratiqueLibelle: String let terrainPratiqueLibelle: String
let pratiques: [Pratique] let pratiques: [String]
let lat, lng: Double let lat, lng: Double
// Method to get the number of courts for a specific sport // Method to get the number of courts for a specific sport
@ -449,7 +456,7 @@ struct ClubMarker: Codable, Hashable, Identifiable {
return courts 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") // Handle cases where only the number of courts is provided (e.g., "2 terrains")
if let courtsNumber = trimmedComponent.split(separator: " ").first, if let courtsNumber = trimmedComponent.split(separator: " ").first,
let courts = Int(courtsNumber) { let courts = Int(courtsNumber) {

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

@ -314,6 +314,7 @@ struct ActivityView: View {
NavigationStack { NavigationStack {
TournamentLookUpView() TournamentLookUpView()
.environment(federalDataViewModel) .environment(federalDataViewModel)
.environment(navigation)
} }
} }
.sheet(item: $newTournament) { tournament in .sheet(item: $newTournament) { tournament in
@ -425,7 +426,11 @@ struct ActivityView: View {
Task { Task {
do { do {
let clubs : [Club] = dataStore.user.clubsObjects() 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 { } catch {
self.error = error self.error = error
} }

@ -333,6 +333,26 @@ struct EventListView: View {
} label: { } label: {
Text("Informations de contact Juge-Arbitre") 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] { private func _nextMonths() -> [Date] {

@ -9,10 +9,12 @@ import SwiftUI
import CoreLocation import CoreLocation
import CoreLocationUI import CoreLocationUI
import PadelClubData import PadelClubData
import LeStorage
struct TournamentLookUpView: View { struct TournamentLookUpView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel @Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
@Environment(NavigationViewModel.self) var navigationViewModel: NavigationViewModel
@StateObject var locationManager = LocationManager() @StateObject var locationManager = LocationManager()
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@FocusState private var isFocused: Bool @FocusState private var isFocused: Bool
@ -27,11 +29,20 @@ struct TournamentLookUpView: View {
@State private var presentAlert: Bool = false @State private var presentAlert: Bool = false
@State private var confirmSearch: Bool = false @State private var confirmSearch: Bool = false
@State private var locationRequested = false @State private var locationRequested = false
@State private var apiError: StoreError?
var tournaments: [FederalTournament] { var tournaments: [FederalTournament] {
federalDataViewModel.searchedFederalTournaments federalDataViewModel.searchedFederalTournaments
} }
var presentApiError: Binding<Bool> {
Binding {
apiError != nil
} set: { value in
}
}
var showLastError: Binding<Bool> { var showLastError: Binding<Bool> {
Binding { Binding {
locationManager.lastError != nil locationManager.lastError != nil
@ -70,6 +81,26 @@ struct TournamentLookUpView: View {
secondaryButton: .cancel() 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: { .alert("Attention", isPresented: $presentAlert, actions: {
Button { Button {
presentAlert = false presentAlert = false
@ -79,8 +110,11 @@ struct TournamentLookUpView: View {
Task { Task {
await getNewPage() await getNewPage()
searching = false searching = false
if apiError == nil {
dismiss() dismiss()
} }
}
} label: { } label: {
Label("Tout voir", systemImage: "arrow.down.circle") Label("Tout voir", systemImage: "arrow.down.circle")
} }
@ -187,13 +221,26 @@ struct TournamentLookUpView: View {
private func _gatherNumbers() { private func _gatherNumbers() {
Task { Task {
print("Doing.....") print("Doing.....")
await withTaskGroup(of: (Int, String?).self) { group in
for i in 0..<tournaments.count { for i in 0..<tournaments.count {
print(i, "/", tournaments.count) let tournamentID = tournaments[i].id
let phone = try? await NetworkFederalService.shared.getUmpireData(idTournament: tournaments[i].id).phone let index = i // Capture index for use in the child task
federalDataViewModel.searchedFederalTournaments[i].updateJapPhoneNumber(phone: phone)
print(federalDataViewModel.searchedFederalTournaments[i].japMessage) group.addTask {
print("Starting task for tournament \(index) / \(self.tournaments.count)")
let phone = try? await NetworkFederalService.shared.getUmpireData(idTournament: tournamentID).phone
return (index, phone) // Return the index along with the phone number
}
} }
// Process results as they complete
for await (index, phone) in group {
var tournamentData = federalDataViewModel.searchedFederalTournaments[index] // Get a mutable copy
tournamentData.updateJapPhoneNumber(phone: phone) // Mutate the copy
federalDataViewModel.searchedFederalTournaments[index] = tournamentData // Assign back
}
}
print(".....Done") print(".....Done")
} }
} }
@ -219,7 +266,7 @@ struct TournamentLookUpView: View {
searching = false searching = false
if tournaments.isEmpty == false && tournaments.count < total && total >= 200 && requestedToGetAllPages == false { if tournaments.isEmpty == false && tournaments.count < total && total >= 200 && requestedToGetAllPages == false {
presentAlert = true presentAlert = true
} else { } else if apiError == nil {
dismiss() dismiss()
} }
} }
@ -251,18 +298,13 @@ struct TournamentLookUpView: View {
func getNewPage() async { func getNewPage() async {
do { do {
if NetworkFederalService.shared.formId.isEmpty {
await getNewBuildForm() 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)
} else { if commands.success == false {
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 federalDataViewModel.lastError = .maintenance
} }
let resultCommand = commands.first(where: { $0.results != nil }) commands.tournaments.forEach { ft in
if let newTournaments = resultCommand?.results?.items {
newTournaments.forEach { ft in
// let isValid = ft.tournaments.anySatisfy({ build in // let isValid = ft.tournaments.anySatisfy({ build in
// let ageValid = ages.isEmpty ? true : ages.contains(build.age) // let ageValid = ages.isEmpty ? true : ages.contains(build.age)
// let levelValid = levels.isEmpty ? true : levels.contains(build.level) // let levelValid = levels.isEmpty ? true : levels.contains(build.level)
@ -273,8 +315,8 @@ struct TournamentLookUpView: View {
federalDataViewModel.searchedFederalTournaments.append(ft) federalDataViewModel.searchedFederalTournaments.append(ft)
} }
} }
}
if let count = resultCommand?.results?.nb_results { let count = commands.totalResults
print("count", count, total, tournaments.count, page) print("count", count, total, tournaments.count, page)
total = count total = count
@ -286,22 +328,11 @@ struct TournamentLookUpView: View {
} else { } else {
print("finished") print("finished")
} }
} else { } catch let error as StoreError {
print("total results not found")
}
}
} catch {
print("getNewPage", error) print("getNewPage", error)
await getNewBuildForm() apiError = error
}
}
func getNewBuildForm() async {
do {
try await NetworkFederalService.shared.getNewBuildForm()
await getNewPage()
} catch { } catch {
print("getNewBuildForm", error) print("getNewPage", error)
} }
} }

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

@ -22,9 +22,15 @@ struct PlanningView: View {
let updatePlannedDatesTip = UpdatePlannedDatesTip() let updatePlannedDatesTip = UpdatePlannedDatesTip()
let allMatches: [Match] let allMatches: [Match]
let timeSlotMoveOptionTip = TimeSlotMoveOptionTip() let timeSlotMoveOptionTip = TimeSlotMoveOptionTip()
let event: Event?
init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>) { init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>, event: Event? = nil) {
self.event = event
if let event {
self.allMatches = event.confirmedTournaments().flatMap { $0.allMatches() }
} else {
self.allMatches = matches self.allMatches = matches
}
_selectedScheduleDestination = selectedScheduleDestination _selectedScheduleDestination = selectedScheduleDestination
} }
@ -132,6 +138,24 @@ struct PlanningView: View {
} else { } else {
ToolbarItemGroup(placement: .topBarTrailing) { ToolbarItemGroup(placement: .topBarTrailing) {
Menu { 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 { if notSlots == false {
CourtOptionsView(timeSlots: timeSlots, underlined: false) CourtOptionsView(timeSlots: timeSlots, underlined: false)
Toggle(isOn: $enableMove) { Toggle(isOn: $enableMove) {
@ -226,10 +250,25 @@ struct PlanningView: View {
RowButtonView("Horaire intelligent") { RowButtonView("Horaire intelligent") {
selectedScheduleDestination = nil 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 { struct BySlotView: View {

@ -18,6 +18,12 @@ struct PlayerDetailView: View {
@State private var licenceId: String @State private var licenceId: String
@State private var phoneNumber: String @State private var phoneNumber: String
@State private var email: 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? @FocusState var focusedField: PlayerRegistration.CodingKeys?
var tournamentStore: TournamentStore? { var tournamentStore: TournamentStore? {
@ -29,6 +35,9 @@ struct PlayerDetailView: View {
_licenceId = .init(wrappedValue: player.licenceId ?? "") _licenceId = .init(wrappedValue: player.licenceId ?? "")
_email = .init(wrappedValue: player.email ?? "") _email = .init(wrappedValue: player.email ?? "")
_phoneNumber = .init(wrappedValue: player.phoneNumber ?? "") _phoneNumber = .init(wrappedValue: player.phoneNumber ?? "")
_contactName = .init(wrappedValue: player.contactName ?? "")
_contactEmail = .init(wrappedValue: player.contactEmail ?? "")
_contactPhoneNumber = .init(wrappedValue: player.contactPhoneNumber ?? "")
} }
var body: some View { var body: some View {
@ -148,7 +157,7 @@ struct PlayerDetailView: View {
} }
} else if player.isMalePlayer() == false && tournament.tournamentCategory == .men, let rank = player.rank { } else if player.isMalePlayer() == false && tournament.tournamentCategory == .men, let rank = player.rank {
Section { 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 { LabeledContent {
Text(value.formatted()) Text(value.formatted())
} label: { } label: {
@ -210,6 +219,18 @@ struct PlayerDetailView: View {
} }
} label: { } label: {
Menu { 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) CopyPasteButtonView(pasteValue: player.phoneNumber)
PasteButtonView(text: $phoneNumber) PasteButtonView(text: $phoneNumber)
} label: { } label: {
@ -231,34 +252,104 @@ struct PlayerDetailView: View {
} }
} label: { } label: {
Menu { Menu {
if let mail = player.email, let mailURL = URL(string: "mail:\(mail)") {
Link(destination: mailURL) {
Label(mail, systemImage: "mail")
}
Divider()
}
CopyPasteButtonView(pasteValue: player.email) CopyPasteButtonView(pasteValue: player.email)
PasteButtonView(text: $email) PasteButtonView(text: $email)
} label: { } label: {
Text("Email") Text("Email")
} }
} }
} header: {
Text("Information fédérale")
} }
Section { Section {
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: "") { LabeledContent {
if let url = URL(string: "tel:\(number)") { 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()
}
} 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) { Link(destination: url) {
Label("Appeler", systemImage: "phone") Label("\(number)", systemImage: "phone")
} }
} }
if let url = URL(string: "sms:\(number)") { if let url = URL(string: "sms:\(number)") {
Link(destination: url) { Link(destination: url) {
Label("Message", systemImage: "message") Label(number, systemImage: "message")
} }
} }
Divider()
} }
if let mail = player.email, let mailURL = URL(string: "mail:\(mail)") { CopyPasteButtonView(pasteValue: player.contactPhoneNumber)
PasteButtonView(text: $contactPhoneNumber)
} label: {
Text("Téléphone contact")
}
}
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) { Link(destination: mailURL) {
Label("Mail", systemImage: "mail") 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 { // Section {
// NavigationLink { // NavigationLink {

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

@ -64,6 +64,7 @@ enum FileImportCustomField: Int, Identifiable, CaseIterable {
struct FileImportView: View { struct FileImportView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) var navigationViewModel: NavigationViewModel
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@ -84,6 +85,15 @@ struct FileImportView: View {
@State private var presentFormatHelperView: Bool = false @State private var presentFormatHelperView: Bool = false
@State private var validatedTournamentIds: Set<String> = Set() @State private var validatedTournamentIds: Set<String> = Set()
@State private var chunkMode: ChunkMode = .byParameter @State private var chunkMode: ChunkMode = .byParameter
@State private var apiError: StoreError?
var presentApiError: Binding<Bool> {
Binding {
apiError != nil
} set: { value in
}
}
enum ChunkMode: Int, Identifiable, CaseIterable { enum ChunkMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue } var id: Int { self.rawValue }
@ -175,7 +185,11 @@ struct FileImportView: View {
if let fileContent { if let fileContent {
do { do {
try await _startImport(fileContent: fileContent, allTournaments: false) try await _startImport(fileContent: fileContent, allTournaments: false)
} catch let error as StoreError {
Logger.error(error)
apiError = error
} catch { } catch {
Logger.error(error)
errorMessage = error.localizedDescription errorMessage = error.localizedDescription
} }
} }
@ -199,7 +213,11 @@ struct FileImportView: View {
if let fileContent { if let fileContent {
do { do {
try await _startImport(fileContent: fileContent, allTournaments: true) try await _startImport(fileContent: fileContent, allTournaments: true)
} catch let error as StoreError {
Logger.error(error)
apiError = error
} catch { } catch {
Logger.error(error)
errorMessage = error.localizedDescription errorMessage = error.localizedDescription
} }
} }
@ -306,7 +324,11 @@ struct FileImportView: View {
if let fileContent { if let fileContent {
do { do {
try await _startImport(fileContent: fileContent, allTournaments: false) try await _startImport(fileContent: fileContent, allTournaments: false)
} catch let error as StoreError {
Logger.error(error)
apiError = error
} catch { } catch {
Logger.error(error)
errorMessage = error.localizedDescription errorMessage = error.localizedDescription
} }
} }
@ -381,6 +403,26 @@ struct FileImportView: View {
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .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) { .sheet(isPresented: $presentFormatHelperView) {
NavigationStack { NavigationStack {
List { List {
@ -434,6 +476,9 @@ struct FileImportView: View {
fileContent = try String(contentsOf: selectedFile) fileContent = try String(contentsOf: selectedFile)
} }
selectedFile.stopAccessingSecurityScopedResource() selectedFile.stopAccessingSecurityScopedResource()
} catch let error as StoreError {
Logger.error(error)
apiError = error
} catch { } catch {
Logger.error(error) Logger.error(error)
errorMessage = error.localizedDescription errorMessage = error.localizedDescription
@ -453,6 +498,7 @@ struct FileImportView: View {
do { do {
fileContent = try String(contentsOf: url) fileContent = try String(contentsOf: url)
} catch { } catch {
Logger.error(error)
errorMessage = error.localizedDescription errorMessage = error.localizedDescription
} }
} }

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

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

@ -28,7 +28,11 @@ struct PrintSettingsView: View {
Section { Section {
// Toggle(isOn: $generator.displayHeads, label: { // Toggle(isOn: $generator.displayHeads, label: {
// Text("Afficher les têtes de séries") // Text("Afficher les têtes de séries")
// }) // }
Toggle(isOn: $generator.displayPlannedDate, label: {
Text("Afficher la date plannifiée")
})
Toggle(isOn: $generator.displayTeamIndex, label: { Toggle(isOn: $generator.displayTeamIndex, label: {
Text("Afficher le poids et le rang de l'équipe") Text("Afficher le poids et le rang de l'équipe")
@ -46,10 +50,16 @@ struct PrintSettingsView: View {
Toggle(isOn: $generator.includeBracket, label: { Toggle(isOn: $generator.includeBracket, label: {
Text("Tableau") Text("Tableau")
}) })
.onChange(of: generator.includeBracket) { oldValue, newValue in
if newValue == false {
generator.includeLoserBracket = newValue
}
}
Toggle(isOn: $generator.includeLoserBracket, label: { Toggle(isOn: $generator.includeLoserBracket, label: {
Text("Tableau des matchs de classements") Text("Tableau des matchs de classements")
}) })
.disabled(generator.includeBracket == false)
if tournament.groupStages().isEmpty == false { if tournament.groupStages().isEmpty == false {
Toggle(isOn: $generator.includeGroupStage, label: { Toggle(isOn: $generator.includeGroupStage, label: {
@ -58,7 +68,6 @@ struct PrintSettingsView: View {
} }
} }
if generator.includeBracket {
Section { Section {
Picker(selection: $generator.zoomLevel) { Picker(selection: $generator.zoomLevel) {
Text("1 page").tag(nil as Optional<CGFloat>) Text("1 page").tag(nil as Optional<CGFloat>)
@ -114,7 +123,7 @@ struct PrintSettingsView: View {
self.prepareGroupStage = false self.prepareGroupStage = false
self.generationId = UUID() self.generationId = UUID()
} }
.disabled(generator.includeBracket == false && generator.includeGroupStage == false && generator.includeLoserBracket == false) .disabled(generator.includeBracket == false && generator.includeGroupStage == false)
} else { } else {
LabeledContent { LabeledContent {
ProgressView() ProgressView()
@ -123,7 +132,6 @@ struct PrintSettingsView: View {
} }
.id(generationId) .id(generationId)
} }
}
Section { Section {
NavigationLink { NavigationLink {
@ -178,7 +186,7 @@ struct PrintSettingsView: View {
} }
if let groupStage = tournament.groupStages().first { 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") Text("Poule")
} }
} }
@ -219,7 +227,7 @@ struct PrintSettingsView: View {
Group { Group {
if prepareGroupStage { if prepareGroupStage {
ForEach(tournament.groupStages()) { groupStage in 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 { if let error {
print("preparePDF", error) print("preparePDF", error)
} else if loaded == false { } else if loaded == false {
@ -321,7 +329,7 @@ struct WebViewPreview: View {
ProgressView() ProgressView()
.onAppear { .onAppear {
if let groupStage { 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 { } else if let round {
html = generator.generateLoserBracketHtml(upperRound: round) html = generator.generateLoserBracketHtml(upperRound: round)
} else { } else {

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

@ -310,7 +310,7 @@ final class ServerDataTests: XCTestCase {
return 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" playerRegistration.storeId = "123"
if let pr: PlayerRegistration = try await StoreCenter.main.service().post(playerRegistration) { 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.timeToConfirm == playerRegistration.timeToConfirm)
assert(pr.registrationStatus == playerRegistration.registrationStatus) assert(pr.registrationStatus == playerRegistration.registrationStatus)
assert(pr.paymentId == playerRegistration.paymentId) assert(pr.paymentId == playerRegistration.paymentId)
assert(pr.contactName == playerRegistration.contactName)
assert(pr.contactEmail == playerRegistration.contactEmail)
assert(pr.contactPhoneNumber == playerRegistration.contactPhoneNumber)
} else { } else {
XCTFail("missing data") XCTFail("missing data")
} }

Loading…
Cancel
Save