ajout de l'import des joueurs de la FFT et l'écran de recherche de joueurs, avec toutes les classes et objets que cela implique

multistore
Razmig Sarkissian 2 years ago
parent 602ae91c40
commit 75d8b1e42b
  1. 175
      PadelClub.xcodeproj/project.pbxproj
  2. 27
      PadelClub/Data/Coredata/PadelClubApp.xcdatamodeld/Model.xcdatamodel/contents
  3. 173
      PadelClub/Data/Coredata/Persistence.swift
  4. 3
      PadelClub/Data/DataStore.swift
  5. 94
      PadelClub/Data/Federal/FederalPlayer.swift
  6. 20
      PadelClub/Extensions/Array+Extensions.swift
  7. 14
      PadelClub/Extensions/Date+Extensions.swift
  8. 25
      PadelClub/Extensions/FixedWidthInteger+Extensions.swift
  9. 16
      PadelClub/Extensions/Sequence+Extensions.swift
  10. 22
      PadelClub/Extensions/String+Extensions.swift
  11. 51
      PadelClub/Extensions/URL+Extensions.swift
  12. 54
      PadelClub/Manager/FileImportManager.swift
  13. 46
      PadelClub/Manager/Network/NetworkManager.swift
  14. 17
      PadelClub/Manager/Network/NetworkManagerError.swift
  15. 73
      PadelClub/Manager/SourceFileManager.swift
  16. 3
      PadelClub/PadelClubApp.swift
  17. 0
      PadelClub/ViewModel/AgendaDestination.swift
  18. 436
      PadelClub/ViewModel/SearchViewModel.swift
  19. 4
      PadelClub/ViewModel/TabDestination.swift
  20. 44
      PadelClub/Views/Components/RowButtonView.swift
  21. 9
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  22. 37
      PadelClub/Views/Navigation/Agenda/EmptyActivityView.swift
  23. 0
      PadelClub/Views/Navigation/Agenda/WelcomeView.swift
  24. 153
      PadelClub/Views/Navigation/MainView.swift
  25. 44
      PadelClub/Views/Navigation/Organizer/TournamentButtonView.swift
  26. 37
      PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift
  27. 145
      PadelClub/Views/Navigation/PadelClubView.swift
  28. 10
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  29. 2
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  30. 71
      PadelClub/Views/Shared/ImportedPlayerView.swift
  31. 442
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  32. 13
      PadelClub/Views/Tournament/Screen/PresentationContext.swift
  33. 13
      PadelClub/Views/Tournament/Screen/Screen.swift
  34. 14
      PadelClub/Views/Tournament/TournamentView.swift
  35. 55
      PadelClub/Views/ViewModifiers/DeferredViewModifier.swift

@ -34,13 +34,27 @@
C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB22B86387500ADC637 /* AccountView.swift */; };
FF2BE4872B85E27400592328 /* LeStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C425D4542B6D24E2002A7B48 /* LeStorage.framework */; };
FF2BE4882B85E27400592328 /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C425D4542B6D24E2002A7B48 /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; };
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; };
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74F52B919E45004CFE0E /* UmpireView.swift */; };
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */; };
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6B42B9248200002987F /* NetworkManager.swift */; };
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */; };
FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; };
FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */; };
FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; };
FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; };
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; };
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; };
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; };
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; };
FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FF2B94794700EA7F5A /* PresentationContext.swift */; };
FF6EC9042B9479F500EA7F5A /* Sequence+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC9032B9479F500EA7F5A /* Sequence+Extensions.swift */; };
FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC9052B947A1000EA7F5A /* NetworkManagerError.swift */; };
FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC9082B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift */; };
FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC90A2B947AC000EA7F5A /* Array+Extensions.swift */; };
FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7091612B90F04300AB08DA /* TournamentOrganizerView.swift */; };
FF7091662B90F0B000AB08DA /* NavigationDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7091652B90F0B000AB08DA /* NavigationDestination.swift */; };
FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7091652B90F0B000AB08DA /* TabDestination.swift */; };
FF7091682B90F79F00AB08DA /* TournamentCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7091672B90F79F00AB08DA /* TournamentCellView.swift */; };
FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7091692B90F95E00AB08DA /* DateBoxView.swift */; };
FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF70916B2B91005400AB08DA /* TournamentView.swift */; };
@ -51,6 +65,13 @@
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; };
FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784012B91C1B4000F62A6 /* WelcomeView.swift */; };
FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784032B91C280000F62A6 /* EmptyActivityView.swift */; };
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; };
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; };
FFF8ACD22B9238C3008466FA /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD12B9238C3008466FA /* FileImportManager.swift */; };
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; };
FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD52B923960008466FA /* URL+Extensions.swift */; };
FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD82B923F3C008466FA /* String+Extensions.swift */; };
FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACDA2B923F48008466FA /* Date+Extensions.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -128,13 +149,27 @@
C4A47DAC2B85FCCD00ADC637 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
C4A47DB02B86375E00ADC637 /* MainUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUserView.swift; sourceTree = "<group>"; };
C4A47DB22B86387500ADC637 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
FF3F74F52B919E45004CFE0E /* UmpireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireView.swift; sourceTree = "<group>"; };
FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaDestination.swift; sourceTree = "<group>"; };
FF4AB6B42B9248200002987F /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; };
FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePlayerListView.swift; sourceTree = "<group>"; };
FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedPlayerView.swift; sourceTree = "<group>"; };
FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = "<group>"; };
FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = "<group>"; };
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; };
FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; };
FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
FF6EC8FF2B94794700EA7F5A /* PresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationContext.swift; sourceTree = "<group>"; };
FF6EC9032B9479F500EA7F5A /* Sequence+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extensions.swift"; sourceTree = "<group>"; };
FF6EC9052B947A1000EA7F5A /* NetworkManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerError.swift; sourceTree = "<group>"; };
FF6EC9082B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+Extensions.swift"; sourceTree = "<group>"; };
FF6EC90A2B947AC000EA7F5A /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = "<group>"; };
FF7091612B90F04300AB08DA /* TournamentOrganizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentOrganizerView.swift; sourceTree = "<group>"; };
FF7091652B90F0B000AB08DA /* NavigationDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationDestination.swift; sourceTree = "<group>"; };
FF7091652B90F0B000AB08DA /* TabDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabDestination.swift; sourceTree = "<group>"; };
FF7091672B90F79F00AB08DA /* TournamentCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentCellView.swift; sourceTree = "<group>"; };
FF7091692B90F95E00AB08DA /* DateBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateBoxView.swift; sourceTree = "<group>"; };
FF70916B2B91005400AB08DA /* TournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentView.swift; sourceTree = "<group>"; };
@ -146,6 +181,13 @@
FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
FFD784012B91C1B4000F62A6 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
FFD784032B91C280000F62A6 /* EmptyActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyActivityView.swift; sourceTree = "<group>"; };
FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = "<group>"; };
FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = "<group>"; };
FFF8ACD12B9238C3008466FA /* FileImportManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = "<group>"; };
FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = "<group>"; };
FFF8ACD52B923960008466FA /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
FFF8ACD82B923F3C008466FA /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
FFF8ACDA2B923F48008466FA /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -201,9 +243,11 @@
C425D44E2B6D24E1002A7B48 /* LeStorage.xcodeproj */,
C425D4002B6D249D002A7B48 /* PadelClubApp.swift */,
FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */,
FF3F74FD2B91A087004CFE0E /* Model */,
C4A47D722B72881500ADC637 /* Views */,
FF3F74FD2B91A087004CFE0E /* ViewModel */,
C4A47D5F2B6D3B2D00ADC637 /* Data */,
FFF8ACD02B9238A2008466FA /* Manager */,
FFF8ACD72B923F26008466FA /* Extensions */,
C425D4042B6D249E002A7B48 /* Assets.xcassets */,
C425D4062B6D249E002A7B48 /* Preview Content */,
);
@ -258,6 +302,8 @@
C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */,
C4A47D592B6D383C00ADC637 /* Tournament.swift */,
C4A47D622B6D3D6500ADC637 /* Club.swift */,
FF6EC9012B94799200EA7F5A /* Coredata */,
FF6EC9022B9479B900EA7F5A /* Federal */,
);
path = Data;
sourceTree = "<group>";
@ -265,13 +311,15 @@
C4A47D722B72881500ADC637 /* Views */ = {
isa = PBXGroup;
children = (
C425D4022B6D249D002A7B48 /* ContentView.swift */,
C4A47D732B72881F00ADC637 /* ClubView.swift */,
FF39719B2B8DE04B004C4E75 /* Navigation */,
FF3F74F72B919F96004CFE0E /* Tournament */,
C4A47D882B7BBB5000ADC637 /* Subscription */,
C4A47D852B7BA33F00ADC637 /* User */,
C425D4022B6D249D002A7B48 /* ContentView.swift */,
C4A47D732B72881F00ADC637 /* ClubView.swift */,
FF6EC8FC2B9478C800EA7F5A /* Shared */,
C4A47DA02B7D0BD800ADC637 /* Components */,
FFDDD40F2B93B2C900C91A49 /* ViewModifiers */,
);
path = Views;
sourceTree = "<group>";
@ -314,6 +362,7 @@
isa = PBXGroup;
children = (
C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */,
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -323,7 +372,6 @@
children = (
FF59FFB62B90EFBF0061EFF9 /* MainView.swift */,
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */,
FFD784012B91C1B4000F62A6 /* WelcomeView.swift */,
FFD783FB2B91B919000F62A6 /* Agenda */,
FF3F74FA2B91A04B004CFE0E /* Organizer */,
FF3F74FB2B91A060004CFE0E /* Toolbox */,
@ -354,6 +402,8 @@
FF3F74F92B91A018004CFE0E /* Screen */ = {
isa = PBXGroup;
children = (
FF6EC8FD2B94792300EA7F5A /* Screen.swift */,
FF6EC8FF2B94794700EA7F5A /* PresentationContext.swift */,
FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */,
);
path = Screen;
@ -364,6 +414,7 @@
children = (
FF7091612B90F04300AB08DA /* TournamentOrganizerView.swift */,
FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */,
FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */,
);
path = Organizer;
sourceTree = "<group>";
@ -384,12 +435,49 @@
path = Umpire;
sourceTree = "<group>";
};
FF3F74FD2B91A087004CFE0E /* Model */ = {
FF3F74FD2B91A087004CFE0E /* ViewModel */ = {
isa = PBXGroup;
children = (
FF7091652B90F0B000AB08DA /* NavigationDestination.swift */,
FF7091652B90F0B000AB08DA /* TabDestination.swift */,
FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */,
FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */,
);
path = Model;
path = ViewModel;
sourceTree = "<group>";
};
FF6EC8FC2B9478C800EA7F5A /* Shared */ = {
isa = PBXGroup;
children = (
FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */,
FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */,
);
path = Shared;
sourceTree = "<group>";
};
FF6EC9012B94799200EA7F5A /* Coredata */ = {
isa = PBXGroup;
children = (
FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */,
FF3795652B9399AA004EA093 /* Persistence.swift */,
);
path = Coredata;
sourceTree = "<group>";
};
FF6EC9022B9479B900EA7F5A /* Federal */ = {
isa = PBXGroup;
children = (
FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */,
);
path = Federal;
sourceTree = "<group>";
};
FF6EC9072B947A1E00EA7F5A /* Network */ = {
isa = PBXGroup;
children = (
FF4AB6B42B9248200002987F /* NetworkManager.swift */,
FF6EC9052B947A1000EA7F5A /* NetworkManagerError.swift */,
);
path = Network;
sourceTree = "<group>";
};
FFD783FB2B91B919000F62A6 /* Agenda */ = {
@ -397,13 +485,44 @@
children = (
FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */,
FFD784032B91C280000F62A6 /* EmptyActivityView.swift */,
FFD784012B91C1B4000F62A6 /* WelcomeView.swift */,
FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */,
FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */,
FFD783FC2B91B9ED000F62A6 /* AgendaDestinationPickerView.swift */,
);
path = Agenda;
sourceTree = "<group>";
};
FFDDD40F2B93B2C900C91A49 /* ViewModifiers */ = {
isa = PBXGroup;
children = (
FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */,
);
path = ViewModifiers;
sourceTree = "<group>";
};
FFF8ACD02B9238A2008466FA /* Manager */ = {
isa = PBXGroup;
children = (
FFF8ACD12B9238C3008466FA /* FileImportManager.swift */,
FFF8ACD32B92392C008466FA /* SourceFileManager.swift */,
FF6EC9072B947A1E00EA7F5A /* Network */,
);
path = Manager;
sourceTree = "<group>";
};
FFF8ACD72B923F26008466FA /* Extensions */ = {
isa = PBXGroup;
children = (
FFF8ACD52B923960008466FA /* URL+Extensions.swift */,
FFF8ACD82B923F3C008466FA /* String+Extensions.swift */,
FFF8ACDA2B923F48008466FA /* Date+Extensions.swift */,
FF6EC9032B9479F500EA7F5A /* Sequence+Extensions.swift */,
FF6EC9082B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift */,
FF6EC90A2B947AC000EA7F5A /* Array+Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -559,41 +678,62 @@
buildActionMask = 2147483647;
files = (
C4A47D872B7BA36D00ADC637 /* UserCreationView.swift in Sources */,
FF7091662B90F0B000AB08DA /* NavigationDestination.swift in Sources */,
FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */,
C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */,
C4A47D8A2B7BBB6500ADC637 /* SubscriptionView.swift in Sources */,
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */,
C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */,
FF7091682B90F79F00AB08DA /* TournamentCellView.swift in Sources */,
FF6EC9042B9479F500EA7F5A /* Sequence+Extensions.swift in Sources */,
C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */,
FFD783FD2B91B9ED000F62A6 /* AgendaDestinationPickerView.swift in Sources */,
FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */,
C4A47DA92B85F82100ADC637 /* ChangePasswordView.swift in Sources */,
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */,
FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */,
FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */,
C4A47D742B72881F00ADC637 /* ClubView.swift in Sources */,
C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */,
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */,
FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */,
FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */,
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */,
C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */,
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */,
C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */,
FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */,
FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */,
C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */,
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */,
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */,
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */,
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */,
FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */,
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */,
FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */,
C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */,
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */,
C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */,
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */,
FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */,
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */,
C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */,
C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */,
FFF8ACD22B9238C3008466FA /* FileImportManager.swift in Sources */,
FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */,
FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */,
FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */,
FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */,
C4A47D922B7BBBEC00ADC637 /* StoreItem.swift in Sources */,
FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */,
C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */,
FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */,
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */,
C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -924,6 +1064,19 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCVersionGroup section */
FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
FF3795612B9396D0004EA093 /* Model.xcdatamodel */,
);
currentVersion = FF3795612B9396D0004EA093 /* Model.xcdatamodel */;
path = PadelClubApp.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
};
rootObject = C425D3F52B6D249D002A7B48 /* Project object */;
}

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23D60" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="ImportedPlayer" representedClassName=".ImportedPlayer" syncable="YES" codeGenerationType="class">
<attribute name="assimilation" attributeType="String"/>
<attribute name="canonicalFirstName" optional="YES" attributeType="String" derived="YES" derivationExpression="canonical:(firstName)"/>
<attribute name="canonicalFullName" optional="YES" attributeType="String" derived="YES" derivationExpression="canonical:(lastName)"/>
<attribute name="canonicalLastName" optional="YES" attributeType="String" derived="YES" derivationExpression="canonical:(lastName)"/>
<attribute name="clubCode" attributeType="String"/>
<attribute name="clubName" attributeType="String"/>
<attribute name="country" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="fullName" attributeType="String"/>
<attribute name="importDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastName" attributeType="String"/>
<attribute name="license" attributeType="String"/>
<attribute name="ligueName" attributeType="String"/>
<attribute name="male" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="points" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="rank" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tournamentCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="license"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

@ -0,0 +1,173 @@
//
// Persistence.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 24/02/2023.
//
import CoreData
class PersistenceController: NSObject {
static let shared = PersistenceController()
private static var _model: NSManagedObjectModel?
private static func model(name: String) throws -> NSManagedObjectModel {
if _model == nil {
_model = try loadModel(name: name, bundle: Bundle.main)
}
return _model!
}
private static func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
throw CoreDataError.modelURLNotFound(forResourceName: name)
}
guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
throw CoreDataError.modelLoadingFailed(forURL: modelURL)
}
return model
}
enum CoreDataError: Error {
case modelURLNotFound(forResourceName: String)
case modelLoadingFailed(forURL: URL)
}
lazy var localContainer : NSPersistentContainer = {
let baseURL = NSPersistentContainer.defaultDirectoryURL()
let storeFolderURL = baseURL.appendingPathComponent("CoreDataStores")
let localStoreFolderURL = storeFolderURL.appendingPathComponent("Local")
let fileManager = FileManager.default
for folderURL in [localStoreFolderURL] where !fileManager.fileExists(atPath: folderURL.path) {
do {
try fileManager.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil)
} catch {
fatalError("#\(#function): Failed to create the store folder: \(error)")
}
}
let container = NSPersistentContainer(name: "PadelClubApp", managedObjectModel: try! Self.model(name: "PadelClubApp"))
guard let localStoreDescription = container.persistentStoreDescriptions.first!.copy() as? NSPersistentStoreDescription else {
fatalError("#\(#function): Copying the private store description returned an unexpected value.")
}
localStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
localStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
localStoreDescription.setValue("DELETE" as NSObject, forPragmaNamed: "journal_mode")
localStoreDescription.url = localStoreFolderURL.appendingPathComponent("local.sqlite")
var storeDescriptions = [localStoreDescription]
/**
Load the persistent stores.
*/
container.persistentStoreDescriptions = storeDescriptions
container.loadPersistentStores(completionHandler: { (loadedStoreDescription, error) in
guard error == nil else {
fatalError("#\(#function): Failed to load persistent stores:\(error!)")
}
// if UserDefaults.standard.string(forKey: "lastDataSource") == nil {
// UserDefaults.standard.setValue("09-2023", forKey: "lastDataSource")
// }
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.name = "viewContext"
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.undoManager = nil
container.viewContext.shouldDeleteInaccessibleFaults = true
container.viewContext.transactionAuthor = PersistenceController.authorName
return container
}()
/// Creates and configures a private queue context.
private func newTaskContext() -> NSManagedObjectContext {
// Create a private queue context.
/// - Tag: newBackgroundContext
let taskContext = localContainer.newBackgroundContext()
taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
// Set unused undoManager to nil for macOS (it is nil by default on iOS)
// to reduce resource requirements.
taskContext.undoManager = nil
return taskContext
}
func batchInsertPlayers(_ importedPlayers: [FederalPlayer], importingDate: Date) async {
guard !importedPlayers.isEmpty else { return }
let context = newTaskContext()
context.performAndWait {
context.transactionAuthor = PersistenceController.remoteDataImportAuthorName
let batchInsert = self.newBatchInsertRequest(with: importedPlayers, importingDate: importingDate)
do {
let result = try context.execute(batchInsert) as? NSBatchInsertResult
if let objectIDs = result?.result as? [NSManagedObjectID], !objectIDs.isEmpty {
let save = [NSInsertedObjectsKey: objectIDs]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: save, into: [localContainer.viewContext])
}
} catch {
print(error.localizedDescription)
}
}
}
private func newBatchInsertRequest(with imported: [FederalPlayer], importingDate: Date)
-> NSBatchInsertRequest {
// 1
var index = 0
let total = imported.count
// 2
let batchInsert = NSBatchInsertRequest(
entity: ImportedPlayer.entity()) { (managedObject: NSManagedObject) -> Bool in
// 3
guard index < total else { return true }
if let importedPlayer = managedObject as? ImportedPlayer {
// 4
let data = imported[index]
importedPlayer.license = data.license
importedPlayer.ligueName = data.ligue
importedPlayer.rank = Int64(data.rank)
importedPlayer.points = data.points ?? 0
importedPlayer.assimilation = data.assimilation
importedPlayer.country = data.country
importedPlayer.tournamentCount = Int64(data.tournamentCount ?? 0)
importedPlayer.lastName = data.lastName
importedPlayer.firstName = data.firstName
importedPlayer.fullName = data.firstName + " " + data.lastName
importedPlayer.clubName = data.club
importedPlayer.clubCode = data.clubCode.replaceCharactersFromSet(characterSet: .whitespaces)
importedPlayer.male = data.isMale
importedPlayer.importDate = importingDate
}
// 5
index += 1
return false
}
return batchInsert
}
// MARK: - History Management
private static let authorName = "Padel Tournament"
private static let remoteDataImportAuthorName = "Data Import"
private func mergeChanges(from transactions: [NSPersistentHistoryTransaction]) {
let context = localContainer.viewContext
context.perform {
transactions.forEach { transaction in
guard let userInfo = transaction.objectIDNotification().userInfo else {
return
}
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: userInfo, into: [context])
}
}
}
}

@ -47,10 +47,9 @@ class DataStore: ObservableObject {
self.clubs = store.registerCollection(synchronized: true)
self.tournaments = store.registerCollection(synchronized: false)
NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidLoad, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidChange, object: nil)
}
@objc func collectionWasUpdated(notification: Notification) {

@ -0,0 +1,94 @@
//
// FederalPlayer.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
extension ImportedPlayer {
var isAssimilated: Bool {
assimilation == "Oui"
}
}
struct FederalPlayer {
var rank: Int
var lastName: String
var firstName: String
var country: String
var license: String
var points: Double?
var assimilation: String
var tournamentCount: Int?
var ligue: String
var clubCode: String
var club: String
var isMale: Bool
var fullNameCanonical: String
/*
;RANG;NOM;PRENOM;Nationalité;N° Licence;POINTS;Assimilation;NB. DE TOURNOIS JOUES;LIGUE;CODE CLUB;CLUB;
*/
var isManPlayer: Bool {
isMale
}
var isAssimilated: Bool {
assimilation == "Oui"
}
var currentRank: Int {
rank
}
init?(_ data: String, isMale: Bool = false) {
self.isMale = isMale
var result = data.components(separatedBy: .newlines).map { $0.trimmed }
result = result.reversed().drop(while: {
$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}).reversed() as [String]
result = Array(result.drop(while: {
$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}))
print(result)
if result.count < 11 {
return nil
}
if let _rank = Int(result[0]) {
rank = _rank
} else {
return nil
}
lastName = result[1]
firstName = result[2]
fullNameCanonical = result[1].canonicalVersion + " " + result[2].canonicalVersion
country = result[3]
license = result[4]
// let matches = result[5].matches(of: try! Regex("[0-9]{1,5}\\.00"))
//
// if matches.count == 1 {
// let pts = result[5][matches.first!.range]
// points = Double(pts.replacingOccurrences(of: ",", with: "."))
// if pts.count < result[5].count {
//
// }
// }
//
points = Double(result[5].replacingOccurrences(of: ",", with: "."))
assimilation = result[6]
tournamentCount = Int(result[7])
ligue = result[8]
clubCode = result[9]
club = result[10]
}
}

@ -0,0 +1,20 @@
//
// Array+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import Foundation
extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
func anySatisfy(_ p: (Element) -> Bool) -> Bool {
return !self.allSatisfy { !p($0) }
}
}

@ -0,0 +1,14 @@
//
// Date+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
extension Date {
var monthYearFormatted: String {
formatted(.dateTime.month(.wide).year(.defaultDigits))
}
}

@ -0,0 +1,25 @@
//
// FixedWidthInteger+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import Foundation
public extension FixedWidthInteger {
func ordinalFormattedSuffix() -> String {
switch self {
case 1: return "er"
default: return "ème"
}
}
func ordinalFormatted() -> String {
self.formatted() + self.ordinalFormattedSuffix()
}
var pluralSuffix: String {
self > 1 ? "s" : ""
}
}

@ -0,0 +1,16 @@
//
// Sequence+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import Foundation
extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted { a, b in
return a[keyPath: keyPath] < b[keyPath: keyPath]
}
}
}

@ -0,0 +1,22 @@
//
// String+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
extension String {
var trimmed: String {
trimmingCharacters(in: .whitespacesAndNewlines)
}
func replaceCharactersFromSet(characterSet: CharacterSet, replacementString: String = "") -> String {
components(separatedBy: characterSet).joined(separator:replacementString)
}
var canonicalVersion: String {
trimmed.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ").folding(options: .diacriticInsensitive, locale: .current).lowercased()
}
}

@ -0,0 +1,51 @@
//
// URL+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
extension URL {
static var savedDateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "DD/MM/yyyy"
return df
}()
static var importDateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "MM-yyyy"
return df
}()
var dateFromPath: Date {
let found = deletingPathExtension().path().components(separatedBy: "-").suffix(2).joined(separator: "-")
if let date = URL.importDateFormatter.date(from: found) {
return date
} else {
return Date()
}
}
var index: Int {
if let i = path().dropLast(12).last?.wholeNumberValue {
return i
}
return 0
}
var manData: Bool {
path().contains("MESSIEURS")
}
var womanData: Bool {
path().contains("DAMES")
}
static var seed: URL? {
Bundle.main.url(forResource: "SeedData", withExtension: nil)
}
}

@ -0,0 +1,54 @@
//
// FileImportManager.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
class FileImportManager {
static let shared = FileImportManager()
func importDataFromFFT() async -> String? {
if let importingDate = SourceFile.mostRecentDateAvailable {
for source in SourceFile.allCases {
for fileURL in source.currentURLs {
let p = readCSV(inputFile: fileURL)
await importingChunkOfPlayers(p, importingDate: importingDate)
}
}
return URL.importDateFormatter.string(from: importingDate)
}
return nil
}
func readCSV(inputFile: URL) -> [FederalPlayer] {
do {
let fileContent = try String(contentsOf: inputFile)
return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData)
} catch {
print("error: \(error)") // to do deal with errors
}
return []
}
func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] {
let lines = fileContent.components(separatedBy: "\n")
return lines.compactMap { line in
if line.components(separatedBy: ";").count < 10 {
} else {
let data = line.components(separatedBy: ";").joined(separator: "\n")
return FederalPlayer(data, isMale: isMale)
}
return nil
}
}
func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async {
for chunk in players.chunked(into: 1000) {
await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate)
}
}
}

@ -0,0 +1,46 @@
//
// NetworkManager.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
class NetworkManager {
static let shared: NetworkManager = NetworkManager()
func removeRankingData(lastDateString: String, fileName: String) {
let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv"
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)")
try? FileManager.default.removeItem(at: destinationFileUrl)
}
func downloadRankingData(lastDateString: String, fileName: String) async throws {
let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv"
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)")
let fileURL = URL(string: "https://padelclub.app/static/\(dateString)")
if FileManager.default.fileExists(atPath: destinationFileUrl.path()) {
return
}
var request = URLRequest(url:fileURL!)
request.addValue("attachment;filename=\(dateString)", forHTTPHeaderField:"Content-Disposition")
request.addValue("text/csv", forHTTPHeaderField: "Content-Type")
let task = try await URLSession.shared.download(for: request)
if let urlResponse = task.1 as? HTTPURLResponse {
print(urlResponse.statusCode)
if urlResponse.statusCode == 200 {
try FileManager.default.copyItem(at: task.0, to: destinationFileUrl)
} else if urlResponse.statusCode == 404 && fileName == "MESSIEURS" {
throw NetworkManagerError.fileNotYetAvailable
}
}
}
}

@ -0,0 +1,17 @@
//
// NetworkManagerError.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import Foundation
enum NetworkManagerError: LocalizedError {
case maintenance
case fileNotYetAvailable
case mailFailed
case mailNotSent //no network no error
case messageFailed
case messageNotSent //no network no error
}

@ -0,0 +1,73 @@
//
// SourceFileManager.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
enum SourceFile: String, CaseIterable {
case dames = "DAMES"
case messieurs = "MESSIEURS"
static var mostRecentDateAvailable: Date? {
allFiles(false).first?.dateFromPath
}
static func removeAllFilesFromServer() {
let docDir = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let allFiles = try! FileManager.default.contentsOfDirectory(at: docDir, includingPropertiesForKeys: nil)
allFiles.filter { $0.pathExtension == "csv" }.forEach { url in
try? FileManager.default.removeItem(at: url)
}
}
static var allFiles: [URL] {
let docDir = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let allFiles = try! FileManager.default.contentsOfDirectory(at: docDir, includingPropertiesForKeys: nil).filter({ url in
url.pathExtension == "csv"
})
return (allFiles + (Bundle.main.urls(forResourcesWithExtension: "csv", subdirectory: nil) ?? [])).sorted(by: \.dateFromPath).reversed()
}
static func allFiles(_ isManPlayer: Bool) -> [URL] {
allFiles.filter({ url in
url.path().contains(isManPlayer ? SourceFile.messieurs.rawValue : SourceFile.dames.rawValue)
})
}
static func allFilesSortedByDate(_ isManPlayer: Bool) -> [URL] {
return allFiles(isManPlayer)
}
var filesFromServer: [URL] {
let docDir = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let allFiles = try! FileManager.default.contentsOfDirectory(at: docDir, includingPropertiesForKeys: nil)
return allFiles.filter{$0.pathExtension == "csv" && $0.path().contains(rawValue)}
}
var currentURLs: [URL] {
var files = Bundle.main.urls(forResourcesWithExtension: "csv", subdirectory: nil)?.filter({ url in
url.path().contains(rawValue)
}) ?? []
files.append(contentsOf: filesFromServer)
if let mostRecent = files.sorted(by: \.dateFromPath).reversed().first {
return files.filter({ $0.dateFromPath == mostRecent.dateFromPath })
} else {
return []
}
}
var isMan: Bool {
switch self {
case .dames:
return false
default:
return true
}
}
}

@ -10,12 +10,15 @@ import LeStorage
@main
struct PadelClubApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
MainView()
.onAppear {
self._onAppear()
}
.environment(\.managedObjectContext, persistenceController.localContainer.viewContext)
}
}

@ -0,0 +1,436 @@
//
// SearchViewModel.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 07/02/2024.
//
import SwiftUI
class SearchViewModel: ObservableObject, Identifiable {
let id: UUID = UUID()
var allowSelection : Int = 0
var user: User? = nil
var codeClub: String? = nil
var clubName: String? = nil
var ligueName: String? = nil
@Published var debouncableText: String = ""
@Published var searchText: String = ""
@Published var task: DispatchWorkItem?
@Published var computedSearchText: String = ""
@Published var tokens = [SearchToken]()
@Published var suggestedTokens = [SearchToken]()
@Published var dataSet: DataSet = .national
@Published var filterOption = PlayerFilterOption.all
@Published var hideAssimilation: Bool = false
@Published var ascending: Bool = true
@Published var sortOption: SortOption = .rank
@Published var selectedPlayers: Set<ImportedPlayer> = Set()
@Published var filterSelectionEnabled: Bool = false
var forcedSearch = false
var mostRecentDate: Date? = nil
var selectionIsOver: Bool {
if allowSingleSelection && selectedPlayers.count == 1 {
return true
} else if allowMultipleSelection && selectedPlayers.count == allowSelection {
return true
}
return false
}
var allowMultipleSelection: Bool {
allowSelection > 1 || allowSelection == -1
}
var allowSingleSelection: Bool {
allowSelection == 1
}
var debounceTrigger: Double {
dataSet == .national ? 0.4 : 0.1
}
var throttleTrigger: Double {
dataSet == .national ? 0.15 : 0.1
}
var contentUnavailableMessage: String {
var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."]
if tokens.isEmpty {
message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois. Dans ce pas, Padel Club ne pourra pas le trouver.")
}
return message.joined(separator: "\n")
}
func getCodeClub() -> String? {
if let codeClub { return codeClub }
// if let userCodeClub = user?.player?.codeClub { return userCodeClub }
return nil
}
func getClubName() -> String? {
if let clubName { return clubName }
// if let userClubName = user?.player?.clubName { return userClubName }
return nil
}
func showIndex() -> Bool {
if dataSet == .national { return false }
if filterOption == .all { return false }
return true
}
func prompt(forDataSet: DataSet) -> String {
switch forDataSet {
case .national:
if let mostRecentDate {
return "base fédérale \(mostRecentDate.monthYearFormatted)"
} else {
return "rechercher"
}
case .ligue:
return "dans cette ligue"
case .club:
return "dans ce club"
case .favorite:
return "dans mes favoris"
}
}
func label(forDataSet: DataSet) -> String {
switch forDataSet {
case .national:
return "National"
case .ligue:
return (ligueName)?.capitalized ?? "Ma ligue"
case .club:
return (clubName)?.capitalized ?? "Mon club"
case .favorite:
return "Mes favoris"
}
}
func words() -> [String] {
searchText.trimmed.components(separatedBy: .whitespaces)
}
func wordsPredicates() -> NSPredicate? {
let words = words()
switch words.count {
case 2:
let predicates = [
NSPredicate(format: "canonicalLastName beginswith[cd] %@ AND canonicalFirstName beginswith[cd] %@", words[0], words[1]),
NSPredicate(format: "canonicalLastName beginswith[cd] %@ AND canonicalFirstName beginswith[cd] %@", words[1], words[0]),
]
return NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
default:
return nil
}
}
func orPredicate() -> NSPredicate? {
var predicates : [NSPredicate] = []
switch tokens.first {
case .none:
if searchText.isEmpty == false {
let wordsPredicates = wordsPredicates()
if let wordsPredicates {
predicates.append(wordsPredicates)
} else {
predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", searchText))
predicates.append(NSPredicate(format: "license contains[cd] %@", searchText))
}
}
case .ligue:
if searchText.isEmpty {
predicates.append(NSPredicate(format: "ligueName == nil"))
} else {
predicates.append(NSPredicate(format: "ligueName contains[cd] %@", searchText))
}
case .club:
if searchText.isEmpty {
predicates.append(NSPredicate(format: "clubName == nil"))
} else {
predicates.append(NSPredicate(format: "clubName contains[cd] %@", searchText))
}
case .rankMoreThan:
if searchText.isEmpty || Int(searchText) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank >= %@", searchText))
}
case .rankLessThan:
if searchText.isEmpty || Int(searchText) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank <= %@", searchText))
}
case .rankBetween:
let values = searchText.components(separatedBy: ",")
if searchText.isEmpty || values.count != 2 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!))
}
}
if predicates.isEmpty {
return nil
}
return NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
}
func predicate() -> NSPredicate? {
var predicates : [NSPredicate?] = [
orPredicate(),
filterOption == .male ?
NSPredicate(format: "male == YES") :
nil,
filterOption == .female ?
NSPredicate(format: "male == NO") :
nil,
]
if let mostRecentDate {
predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
}
if hideAssimilation {
predicates.append(NSPredicate(format: "assimilation == %@", "Non"))
}
switch dataSet {
case .national:
break
case .ligue:
if let ligueName {
predicates.append(NSPredicate(format: "ligueName == %@", ligueName))
} else {
predicates.append(NSPredicate(format: "ligueName == nil"))
}
case .club:
if let codeClub {
predicates.append(NSPredicate(format: "clubCode == %@", codeClub))
} else {
predicates.append(NSPredicate(format: "clubCode == nil"))
}
case .favorite:
predicates.append(NSPredicate(format: "license == nil"))
}
return NSCompoundPredicate(andPredicateWithSubpredicates: predicates.compactMap({ $0 }))
}
func sortDescriptors() -> [SortDescriptor<ImportedPlayer>] {
sortOption.sortDescriptors(ascending, dataSet: dataSet)
}
func nsSortDescriptors() -> [NSSortDescriptor] {
sortDescriptors().map { NSSortDescriptor($0) }
}
}
enum SearchToken: String, CaseIterable, Identifiable {
case club = "club"
case ligue = "ligue"
case rankMoreThan = "rang >"
case rankLessThan = "rang <"
case rankBetween = "rang <>"
var id: String {
rawValue
}
var message: String {
switch self {
case .club:
return "Taper le nom d'un club pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois."
case .ligue:
return "Taper le nom d'une ligue pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois."
case .rankMoreThan:
return "Taper un nombre pour chercher les joueurs ayant un classement supérieur ou égale."
case .rankLessThan:
return "Taper un nombre pour chercher les joueurs ayant un classement inférieur ou égale."
case .rankBetween:
return "Taper deux nombres séparés par une virgule pour chercher les joueurs dans cette intervalle de classement"
}
}
var titleLabel: String {
switch self {
case .club:
return "Chercher un club"
case .ligue:
return "Chercher une ligue"
case .rankMoreThan, .rankLessThan:
return "Chercher un rang"
case .rankBetween:
return "Chercher une intervalle de classement"
}
}
var localizedLabel: String {
switch self {
case .club:
return "Club"
case .ligue:
return "Ligue"
case .rankMoreThan:
return "Rang supérieur ou égale à"
case .rankLessThan:
return "Rang inférieur ou égale à"
case .rankBetween:
return "Rang entre deux valeurs"
}
}
var shortLocalizedLabel: String {
switch self {
case .club:
return "Club"
case .ligue:
return "Ligue"
case .rankMoreThan:
return "Rang ≥"
case .rankLessThan:
return "Rang ≤"
case .rankBetween:
return "Rang ≥,≤"
}
}
func icon() -> String {
switch self {
case .club:
return "house.and.flag.fill"
case .ligue:
return "house.and.flag.fill"
case .rankMoreThan:
return "figure.racquetball"
case .rankLessThan:
return "figure.racquetball"
case .rankBetween:
return "figure.racquetball"
}
}
var systemImage: String {
switch self {
case .club:
return "house.and.flag.fill"
case .ligue:
return "house.and.flag.fill"
case .rankMoreThan:
return "figure.racquetball"
case .rankLessThan:
return "figure.racquetball"
case .rankBetween:
return "figure.racquetball"
}
}
}
enum DataSet: Int, CaseIterable, Identifiable {
case national
case ligue
case club
case favorite
var id: Int { rawValue }
var localizedLabel: String {
switch self {
case .national:
return "National"
case .ligue:
return "Ligue"
case .club:
return "Club"
case .favorite:
return "Favori"
}
}
var tokens: [SearchToken] {
switch self {
case .national:
return [.club, .ligue, .rankMoreThan, .rankLessThan, .rankBetween]
case .ligue:
return [.club, .rankMoreThan, .rankLessThan, .rankBetween]
case .club:
return [.rankMoreThan, .rankLessThan, .rankBetween]
case .favorite:
return [.rankMoreThan, .rankLessThan, .rankBetween]
}
}
}
enum SortOption: Int, CaseIterable, Identifiable {
case name
case rank
case tournamentCount
case points
var id: Int { self.rawValue }
var localizedLabel: String {
switch self {
case .name:
return "Nom"
case .rank:
return "Rang"
case .tournamentCount:
return "Tournoi"
case .points:
return "Points"
}
}
func sortDescriptors(_ ascending: Bool, dataSet: DataSet) -> [SortDescriptor<ImportedPlayer>] {
switch self {
case .name:
return [SortDescriptor(\ImportedPlayer.lastName, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation)]
case .rank:
if dataSet == .national {
return [SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse)]
} else {
return [SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)]
}
case .tournamentCount:
return [SortDescriptor(\ImportedPlayer.tournamentCount, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)]
case .points:
return [SortDescriptor(\ImportedPlayer.points, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)]
}
}
}
enum PlayerFilterOption: Int, Hashable, CaseIterable, Identifiable {
case all = -1
case male = 1
case female = 0
var id: Int { rawValue }
func icon() -> String {
switch self {
case .all:
return "Tous"
case .male:
return "Homme"
case .female:
return "Femme"
}
}
var localizedPlayerLabel: String {
switch self {
case .female:
return "joueuse"
default:
return "joueur"
}
}
}

@ -1,5 +1,5 @@
//
// NavigationDestination.swift
// TabDestination.swift
// PadelClub
//
// Created by Razmig Sarkissian on 29/02/2024.
@ -7,7 +7,7 @@
import Foundation
enum NavigationDestination: CaseIterable, Identifiable {
enum TabDestination: CaseIterable, Identifiable {
var id: Self {
self
}

@ -0,0 +1,44 @@
//
// RowButtonView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import SwiftUI
struct RowButtonView: View {
let title: String
var systemImage: String? = nil
var image: String? = nil
let action: () -> ()
var body: some View {
Button {
action()
} label: {
HStack {
Spacer()
if let systemImage {
Image(systemName: systemImage)
}
if let image {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 32, height: 32)
}
Text(title)
.foregroundColor(.white)
.frame(height: 32)
Spacer()
}
.font(.headline)
}
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
.tint(.launchScreenBackground)
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(.zero))
}
}

@ -90,22 +90,25 @@ struct ActivityView: View {
}
ToolbarItemGroup(placement: .bottomBar) {
ToolbarItem(placement: .topBarLeading) {
Button {
filterEnabled.toggle()
} label: {
Label("Vues", systemImage: "line.3.horizontal.decrease.circle")
.symbolVariant(filterEnabled ? .fill : .none)
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {
} label: {
Label("Ajouter", systemImage: "plus")
Label("Ajouter", systemImage: "plus.circle.fill")
}
}
}
}
.navigationTitle(NavigationDestination.activity.title)
.navigationTitle(TabDestination.activity.title)
.navigationDestination(for: Tournament.self) { tournament in
TournamentView(tournament: tournament)
}

@ -6,43 +6,6 @@
//
import SwiftUI
struct RowButtonView: View {
let title: String
var systemImage: String? = nil
var image: String? = nil
let action: () -> ()
var body: some View {
Button {
action()
} label: {
HStack {
Spacer()
if let systemImage {
Image(systemName: systemImage)
}
if let image {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 32, height: 32)
}
Text(title)
.foregroundColor(.white)
.frame(height: 32)
Spacer()
}
.font(.headline)
}
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
.tint(.launchScreenBackground)
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(.zero))
}
}
struct EmptyActivityView: View {
var body: some View {

@ -9,7 +9,29 @@ import SwiftUI
struct MainView: View {
@StateObject var dataStore = DataStore.shared
@AppStorage("importingFiles") var importingFiles: Bool = false
@State private var checkingFilesAttempt: Int = 0
@State private var checkingFiles: Bool = false
@AppStorage("lastDataSource") var lastDataSource: String?
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [],
animation: .default)
private var players: FetchedResults<ImportedPlayer>
var _mostRecentDateAvailable: Date? {
SourceFile.mostRecentDateAvailable
}
var _lastDataSourceDate: Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
var body: some View {
TabView {
if dataStore.tournaments.isEmpty {
@ -29,23 +51,146 @@ struct MainView: View {
.tabItem(for: .padelClub)
}
.environmentObject(dataStore)
.task {
await self._checkSourceFileAvailability()
}
.refreshable {
Task {
await self._checkSourceFileAvailability()
}
}
.overlay(alignment: .bottom) {
if importingFiles {
_activityStatusBoxView()
} else {
_activityStatusBoxView()
.deferredRendering(for: .seconds(5))
}
}
}
func _activityStatusBoxView() -> some View {
_activityStatus()
.font(.title3)
.frame(height: 32)
.padding()
.background {
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(.white)
}
.shadow(radius: 2)
.offset(y: -64)
}
@ViewBuilder
func _activityStatus() -> some View {
if importingFiles {
HStack(spacing: 20) {
ProgressView()
if let _mostRecentDateAvailable {
if _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast {
Text("import " + _mostRecentDateAvailable.monthYearFormatted)
}
}
}
} else if let _mostRecentDateAvailable {
if _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast {
Label(_mostRecentDateAvailable.monthYearFormatted + " disponible", systemImage: "exclamationmark.triangle")
.labelStyle(.titleAndIcon)
} else {
Label(_mostRecentDateAvailable.monthYearFormatted, systemImage: "checkmark")
.labelStyle(.titleAndIcon)
}
}
}
private func _checkSourceFileAvailability() async {
print("check internet")
print("check files on internet")
print("check if any files on internet are more recent than here")
checkingFiles = true
await fetchData()
checkingFilesAttempt += 1
checkingFiles = false
if let _mostRecentDateAvailable, _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast {
_startImporting()
}
}
private func _startImporting() {
importingFiles = true
Task {
lastDataSource = await FileImportManager.shared.importDataFromFFT()
importingFiles = false
}
}
private func fetchData() async {
if let mostRecent = SourceFile.mostRecentDateAvailable, let current = Calendar.current.date(byAdding: .month, value: 1, to: mostRecent), current > mostRecent {
await fetchData(fromDate: current)
} else {
await fetchData(fromDate: Date())
}
}
private func _removeAllData(fromDate current: Date) {
let lastStringDate = URL.importDateFormatter.string(from: current)
let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"]
files.forEach { fileName in
NetworkManager.shared.removeRankingData(lastDateString: lastStringDate, fileName: fileName)
}
}
private func fetchData(fromDate current: Date) async {
let lastStringDate = URL.importDateFormatter.string(from: current)
let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"]
do {
try await withThrowingTaskGroup(of: Void.self) { group in // Mark 1
for file in files {
group.addTask {
try await NetworkManager.shared.downloadRankingData(lastDateString: lastStringDate, fileName: file)
}
}
try await group.waitForAll()
}
if current < Date() {
if let nextCurrent = Calendar.current.date(byAdding: .month, value: 1, to: current) {
await fetchData(fromDate: nextCurrent)
}
}
} catch {
print("downloadRankingData", error)
if _mostRecentDateAvailable == nil {
if let previousDate = Calendar.current.date(byAdding: .month, value: -1, to: current) {
await fetchData(fromDate: previousDate)
}
}
}
}
}
fileprivate extension View {
func tabItem(for navigationDestination: NavigationDestination) -> some View {
modifier(TabItemModifier(navigationDestination: navigationDestination))
func tabItem(for tabDestination: TabDestination) -> some View {
modifier(TabItemModifier(tabDestination: tabDestination))
}
}
fileprivate struct TabItemModifier: ViewModifier {
let navigationDestination: NavigationDestination
let tabDestination: TabDestination
func body(content: Content) -> some View {
content
.tabItem {
Label(navigationDestination.title, systemImage: navigationDestination.image)
Label(tabDestination.title, systemImage: tabDestination.image)
}
}
}

@ -0,0 +1,44 @@
//
// TournamentButtonView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import SwiftUI
struct TournamentButtonView: View {
let tournament: Tournament
@Binding var selectedId: String?
var body: some View {
Button {
if selectedId == tournament.id {
tournament.navigationPath.removeAll()
selectedId = nil
// if tournament.navigationPath.isEmpty {
// selectedId = nil
// } else {
// tournament.navigationPath.removeLast()
// }
} else {
selectedId = tournament.id
}
} label: {
TournamentCellView(tournament: tournament)
.padding(8)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.black, lineWidth: 2)
)
.fixedSize(horizontal: false, vertical: true)
}
.overlay(alignment: .top) {
if selectedId == tournament.id {
Image(systemName: "ellipsis")
.offset(y: -10)
}
}
}
}

@ -39,43 +39,6 @@ struct TournamentOrganizerView: View {
}
}
}
struct TournamentButtonView: View {
let tournament: Tournament
@Binding var selectedId: String?
var body: some View {
Button {
if selectedId == tournament.id {
tournament.navigationPath.removeAll()
selectedId = nil
// if tournament.navigationPath.isEmpty {
// selectedId = nil
// } else {
// tournament.navigationPath.removeLast()
// }
} else {
selectedId = tournament.id
}
} label: {
TournamentCellView(tournament: tournament)
.padding(8)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.black, lineWidth: 2)
)
.fixedSize(horizontal: false, vertical: true)
}
.overlay(alignment: .top) {
if selectedId == tournament.id {
Image(systemName: "ellipsis")
.offset(y: -10)
}
}
}
}
}
#Preview {

@ -6,14 +6,155 @@
//
import SwiftUI
import SwiftData
struct PadelClubView: View {
@State private var checkingFilesAttempt: Int = 0
@State private var checkingFiles: Bool = false
@State private var importingFiles: Bool = false
@AppStorage("lastDataSource") var lastDataSource: String?
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [],
animation: .default)
private var players: FetchedResults<ImportedPlayer>
var _mostRecentDateAvailable: Date? {
SourceFile.mostRecentDateAvailable
}
var _lastDataSourceDate: Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
var body: some View {
NavigationStack {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
.navigationTitle(NavigationDestination.padelClub.title)
List {
if let _lastDataSourceDate {
Section {
HStack {
VStack(alignment: .leading) {
Text("Classement mensuel utilisé").font(.caption).foregroundStyle(.secondary)
Text(_lastDataSourceDate.monthYearFormatted)
}
Spacer()
Image(systemName: "checkmark")
}
}
}
//
// if players.isEmpty {
// ContentUnavailableView {
// Label("Aucun joueur importé", systemImage: "person.slash")
// } description: {
// Text("Padel peut importer toutes les données publique de la FFT concernant tous les compétiteurs et compétitrices.")
// } actions: {
// RowButtonView(title: "Démarrer l'importation") {
// _startImporting()
// }
// }
// }
}
.navigationTitle(TabDestination.padelClub.title)
// .task {
// await self._checkSourceFileAvailability()
// }
// .refreshable {
// Task {
// await self._checkSourceFileAvailability()
// }
// }
}
}
@ViewBuilder
func _activityStatus() -> some View {
if checkingFiles || importingFiles {
ProgressView()
} else if let _mostRecentDateAvailable {
if _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast {
Text(_mostRecentDateAvailable.monthYearFormatted + " disponible à l'importation")
} else {
Label(_mostRecentDateAvailable.monthYearFormatted, systemImage: "checkmark").labelStyle(.titleAndIcon)
}
} else {
Text("Aucune donnée disponible")
}
}
private func _checkSourceFileAvailability() async {
print("check internet")
print("check files on internet")
print("check if any files on internet are more recent than here")
checkingFiles = true
await fetchData()
checkingFilesAttempt += 1
checkingFiles = false
}
private func _startImporting() {
importingFiles = true
Task {
lastDataSource = await FileImportManager.shared.importDataFromFFT()
importingFiles = false
}
}
private func fetchData() async {
if let mostRecent = SourceFile.mostRecentDateAvailable, let current = Calendar.current.date(byAdding: .month, value: 1, to: mostRecent), current > mostRecent {
await fetchData(fromDate: current)
} else {
await fetchData(fromDate: Date())
}
}
private func _removeAllData(fromDate current: Date) {
let lastStringDate = URL.importDateFormatter.string(from: current)
let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"]
files.forEach { fileName in
NetworkManager.shared.removeRankingData(lastDateString: lastStringDate, fileName: fileName)
}
}
private func fetchData(fromDate current: Date) async {
let lastStringDate = URL.importDateFormatter.string(from: current)
let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"]
do {
try await withThrowingTaskGroup(of: Void.self) { group in // Mark 1
for file in files {
group.addTask {
try await NetworkManager.shared.downloadRankingData(lastDateString: lastStringDate, fileName: file)
}
}
try await group.waitForAll()
}
if current < Date() {
if let nextCurrent = Calendar.current.date(byAdding: .month, value: 1, to: current) {
await fetchData(fromDate: nextCurrent)
}
}
} catch {
print("downloadRankingData", error)
if _mostRecentDateAvailable == nil {
if let previousDate = Calendar.current.date(byAdding: .month, value: -1, to: current) {
await fetchData(fromDate: previousDate)
}
}
}
}
}
#Preview {

@ -10,8 +10,14 @@ import SwiftUI
struct ToolboxView: View {
var body: some View {
NavigationStack {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
.navigationTitle(NavigationDestination.toolbox.title)
List {
NavigationLink {
SelectablePlayerListView()
} label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
}
}
.navigationTitle(TabDestination.toolbox.title)
}
}
}

@ -22,8 +22,8 @@ struct UmpireView: View {
} label: {
Label("Abonnement", systemImage: "tennisball.circle.fill")
}
}
.navigationTitle("Juge-Arbitre")
}
}
}

@ -0,0 +1,71 @@
//
// ImportedPlayerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import SwiftUI
struct ImportedPlayerView: View {
let player: ImportedPlayer
var hideLigue: Bool = false
var hideClub: Bool = false
var hidePoints: Bool = false
var index: Int? = nil
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(player.lastName!.capitalized)
Text(player.firstName!.capitalized)
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.rank.formatted()).italic(player.isAssimilated)
.font(.title3)
Text(player.rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
if hidePoints == false {
HStack(alignment: .lastTextBaseline, spacing: 0) {
let pts = player.points
if pts > 0 {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
HStack(alignment: .lastTextBaseline, spacing: 0) {
if player.tournamentCount > 0 {
Text(player.tournamentCount.formatted()).font(.title3)
Text(" tournoi" + player.tournamentCount.pluralSuffix).font(.caption)
}
}
}
}
Text(player.clubName!)
.font(.caption)
Text(player.ligueName!)
.font(.caption)
}
}
}

@ -0,0 +1,442 @@
//
// SelectablePlayerListView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 10/02/2024.
//
import SwiftUI
import CoreData
import Combine
import LeStorage
typealias PlayerSelectionAction = ((Set<ImportedPlayer>) -> ())
typealias ContentUnavailableAction = ((SearchViewModel) -> ())
struct SelectablePlayerListView: View {
let allowSelection: Int
let playerSelectionAction: PlayerSelectionAction?
let contentUnavailableAction: ContentUnavailableAction?
@StateObject private var searchViewModel: SearchViewModel
@Environment(\.dismiss) var dismiss
@AppStorage("lastDataSource") var lastDataSource: String?
@AppStorage("importingFiles") var importingFiles: Bool = false
@State private var searchText: String = ""
var mostRecentDate: Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
init(allowSelection: Int = 0, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) {
self.allowSelection = allowSelection
self.playerSelectionAction = playerSelectionAction
self.contentUnavailableAction = contentUnavailableAction
let searchViewModel = SearchViewModel()
searchViewModel.user = user
searchViewModel.allowSelection = allowSelection
searchViewModel.codeClub = fromPlayer?.clubCode ?? codeClub
searchViewModel.clubName = nil
searchViewModel.ligueName = fromPlayer?.ligue ?? ligue
searchViewModel.dataSet = dataSet
searchViewModel.filterOption = fromPlayer == nil ? filterOption : fromPlayer!.isManPlayer ? .male : .female
searchViewModel.hideAssimilation = hideAssimilation
searchViewModel.ascending = ascending
searchViewModel.sortOption = sortOption
_searchViewModel = StateObject(wrappedValue: searchViewModel)
}
var body: some View {
VStack(spacing: 0) {
if importingFiles == false {
if searchViewModel.filterSelectionEnabled == false {
Picker(selection: $searchViewModel.filterOption) {
ForEach(PlayerFilterOption.allCases, id: \.self) { scope in
Text(scope.icon().capitalized)
}
} label: {
}
.pickerStyle(.segmented)
.padding(.bottom)
.padding(.horizontal)
.background(Material.thick)
Divider()
}
MySearchView(searchViewModel: searchViewModel, contentUnavailableAction: contentUnavailableAction)
.environment(\.editMode, searchViewModel.allowMultipleSelection ? .constant(.active) : .constant(.inactive))
.searchable(text: $searchViewModel.debouncableText, tokens: $searchViewModel.tokens, suggestedTokens: $searchViewModel.suggestedTokens, placement: .navigationBarDrawer(displayMode: .always), prompt: searchViewModel.prompt(forDataSet: searchViewModel.dataSet), token: { token in
Text(token.shortLocalizedLabel)
})
// .searchSuggestions({
// ForEach(searchViewModel.suggestedTokens) { token in
// Button {
// searchViewModel.tokens.append(token)
// } label: {
// Label(token.localizedLabel, systemImage: token.icon())
// }
// }
// })
.onReceive(
searchViewModel.$debouncableText
.debounce(for: .seconds(searchViewModel.debounceTrigger), scheduler: DispatchQueue.main)
.throttle(for: .seconds(searchViewModel.throttleTrigger), scheduler: DispatchQueue.main, latest: true)
) {
guard !$0.isEmpty else {
if searchViewModel.searchText.isEmpty == false {
searchViewModel.searchText = ""
}
return
}
print(">> searching for: \($0)")
searchViewModel.searchText = $0
}
.scrollDismissesKeyboard(.immediately)
.navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection)
// .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor)
.interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false)
.navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet))
.navigationBarTitleDisplayMode(.inline)
} else {
List {
}
}
}
.id(importingFiles)
.overlay {
if let importedFile = SourceFile.mostRecentDateAvailable, importingFiles {
ContentUnavailableView("Importation en cours", systemImage: "square.and.arrow.down", description: Text("Padel Club récupère les données de \(importedFile.monthYearFormatted)"))
}
}
.onAppear {
searchViewModel.mostRecentDate = mostRecentDate
if searchViewModel.tokens.isEmpty {
searchViewModel.debouncableText.removeAll()
searchViewModel.searchText.removeAll()
}
searchViewModel.allowSelection = allowSelection
searchViewModel.selectedPlayers.removeAll()
searchViewModel.hideAssimilation = false
searchViewModel.ascending = true
searchViewModel.sortOption = .rank
searchViewModel.suggestedTokens = searchViewModel.dataSet.tokens
// searchViewModel.fromPlayer = nil
// searchViewModel.codeClub = nil
// searchViewModel.ligueName = nil
// searchViewModel.user = nil
// searchViewModel.dataSet = .national
// searchViewModel.filterOption = .all
}
.onChange(of: searchViewModel.selectedPlayers) {
if let playerSelectionAction, searchViewModel.selectionIsOver {
playerSelectionAction(searchViewModel.selectedPlayers)
dismiss()
}
if searchViewModel.tokens.isEmpty && searchViewModel.searchText.isEmpty == false {
searchViewModel.debouncableText = ""
}
if searchViewModel.selectedPlayers.isEmpty && searchViewModel.filterSelectionEnabled {
searchViewModel.filterSelectionEnabled = false
}
}
.onChange(of: searchViewModel.dataSet) {
searchViewModel.suggestedTokens = searchViewModel.dataSet.tokens
if searchViewModel.filterSelectionEnabled {
searchViewModel.filterSelectionEnabled = false
}
}
.toolbar {
if searchViewModel.allowMultipleSelection {
ToolbarItemGroup(placement: .topBarLeading) {
Button(role: .cancel) {
searchViewModel.selectedPlayers.removeAll()
dismiss()
} label: {
Text("Annuler")
}
}
}
}
// .modifierWithCondition(searchViewModel.user != nil) { thisView in
// thisView
.toolbarTitleMenu {
Picker(selection: $searchViewModel.dataSet) {
ForEach(DataSet.allCases) { dataSet in
Text(searchViewModel.label(forDataSet: dataSet)).tag(dataSet)
}
} label: {
}
}
// }
// .bottomBarAlternative(hide: searchViewModel.selectedPlayers.isEmpty) {
// ZStack {
// HStack{
// Button {
// searchViewModel.filterSelectionEnabled.toggle()
// } label: {
// if searchViewModel.filterSelectionEnabled {
// Image(systemName: "line.3.horizontal.decrease.circle.fill")
// } else {
// Image(systemName: "line.3.horizontal.decrease.circle")
// }
// }
// Spacer()
// }
// Button {
// if let playerSelectionAction {
// playerSelectionAction(searchViewModel.selectedPlayers)
// }
// dismiss()
// } label: {
// Text("Ajouter le" + searchViewModel.selectedPlayers.count.pluralSuffix + " \(searchViewModel.selectedPlayers.count) joueur" + searchViewModel.selectedPlayers.count.pluralSuffix)
// }
// .buttonStyle(.borderedProminent)
// }
// }
}
}
struct MySearchView: View {
@Environment(\.isSearching) private var isSearching
@Environment(\.dismissSearch) private var dismissSearch
@Environment(\.editMode) var editMode
@ObservedObject var searchViewModel: SearchViewModel
@FetchRequest private var players: FetchedResults<ImportedPlayer>
let contentUnavailableAction: ContentUnavailableAction?
init(searchViewModel: SearchViewModel, contentUnavailableAction: ContentUnavailableAction? = nil) {
self.contentUnavailableAction = contentUnavailableAction
_searchViewModel = ObservedObject(wrappedValue: searchViewModel)
_players = FetchRequest<ImportedPlayer>(sortDescriptors: searchViewModel.sortDescriptors(), predicate: searchViewModel.predicate())
}
var body: some View {
playersView
.overlay {
overlayView()
}
.onChange(of: isSearching) {
if isSearching && searchViewModel.filterSelectionEnabled {
searchViewModel.filterSelectionEnabled = false
}
}
.onChange(of: searchViewModel.filterSelectionEnabled) {
if searchViewModel.filterSelectionEnabled && isSearching {
dismissSearch()
}
}
.listStyle(.grouped)
.headerProminence(.increased)
.scrollDismissesKeyboard(.immediately)
.onChange(of: searchViewModel.searchText) {
search()
}
.onChange(of: searchViewModel.filterOption) {
search()
}
.onChange(of: searchViewModel.dataSet) {
search()
}
.onChange(of: searchViewModel.sortOption) {
sort()
}
.onChange(of: searchViewModel.ascending) {
sort()
}
.onChange(of: searchViewModel.hideAssimilation) {
search()
}
}
var specificBugFixUUID: String {
if searchViewModel.dataSet == .national {
return UUID().uuidString
} else {
if searchViewModel.tokens.isEmpty && isSearching {
return UUID().uuidString
}
return "specificBugFixUUID"
}
}
@ViewBuilder
var playersView: some View {
if searchViewModel.allowMultipleSelection {
List(selection: $searchViewModel.selectedPlayers) {
if searchViewModel.filterSelectionEnabled {
let array = Array(searchViewModel.selectedPlayers)
Section {
ForEach(array) { player in
ImportedPlayerView(player: player)
}
.onDelete { indexSet in
for index in indexSet {
let p = array[index]
searchViewModel.selectedPlayers.remove(p)
}
}
} header: {
Text(searchViewModel.selectedPlayers.count.formatted() + " " + searchViewModel.filterOption.localizedPlayerLabel + searchViewModel.selectedPlayers.count.pluralSuffix)
}
} else {
Section {
ForEach(players, id: \.self) { player in
ImportedPlayerView(player: player, index: nil)
}
} header: {
if players.isEmpty == false {
headerView()
}
}
}
}
.id(specificBugFixUUID)
} else {
List {
if searchViewModel.dataSet == .national {
if searchViewModel.allowSingleSelection {
Section {
ForEach(players) { player in
Button {
searchViewModel.selectedPlayers.insert(player)
} label: {
ImportedPlayerView(player: player)
}
.buttonStyle(.plain)
}
} header: {
if players.isEmpty == false {
headerView()
}
}
.id(UUID())
} else {
Section {
ForEach(players) { player in
ImportedPlayerView(player: player)
}
} header: {
if players.isEmpty == false {
headerView()
}
}
.id(UUID())
}
} else {
Section {
ForEach(players.indices, id: \.self) { index in
let player = players[index]
if searchViewModel.allowSingleSelection {
Button {
searchViewModel.selectedPlayers.insert(player)
} label: {
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil)
.contentShape(Rectangle())
}
.frame(maxWidth: .infinity)
.buttonStyle(.plain)
} else {
ImportedPlayerView(player: player)
}
}
} header: {
if players.isEmpty == false {
headerView()
}
}
.id(UUID())
}
}
}
}
private func headerView() -> some View {
HStack {
Text(players.count.formatted() + " " + searchViewModel.filterOption.localizedPlayerLabel + players.count.pluralSuffix)
Spacer()
Menu {
Section {
ForEach(SortOption.allCases) { option in
Toggle(isOn: .init(get: {
return searchViewModel.sortOption == option
}, set: { value in
if searchViewModel.sortOption == option {
searchViewModel.ascending.toggle()
}
searchViewModel.sortOption = option
})) {
Label(option.localizedLabel, systemImage: searchViewModel.sortOption == option ? (searchViewModel.ascending ? "chevron.up" : "chevron.down") : "")
}
}
} header: {
Text("Trier par")
}
Divider()
Section {
Toggle(isOn: .init(get: {
return searchViewModel.hideAssimilation == false
}, set: { value in
searchViewModel.hideAssimilation.toggle()
})) {
Text("Afficher")
}
Toggle(isOn: .init(get: {
return searchViewModel.hideAssimilation == true
}, set: { value in
searchViewModel.hideAssimilation.toggle()
})) {
Text("Masquer")
}
} header: {
Text("Assimilés")
}
} label: {
Label(searchViewModel.sortOption.localizedLabel, systemImage: searchViewModel.ascending ? "chevron.up" : "chevron.down")
}
}
}
@ViewBuilder
func overlayView() -> some View {
if let token = searchViewModel.tokens.first, searchViewModel.searchText.isEmpty {
ContentUnavailableView(token.titleLabel, systemImage: token.systemImage, description: Text(token.message))
} else if players.isEmpty && searchViewModel.filterSelectionEnabled == false && searchViewModel.searchText.isEmpty == false {
ContentUnavailableView {
Label("Aucun résultat pour «\(searchViewModel.searchText)»", systemImage: "magnifyingglass")
} description: {
Text(searchViewModel.contentUnavailableMessage)
} actions: {
Button {
searchViewModel.debouncableText = ""
} label: {
Text("lancer une nouvelle recherche")
}
if let contentUnavailableAction {
Button {
contentUnavailableAction(searchViewModel)
} label: {
Text("créer \(searchViewModel.searchText)")
}
}
}
}
}
private func search() {
//players.nsPredicate = searchViewModel.predicate()
}
private func sort() {
//players.nsSortDescriptors = searchViewModel.nsSortDescriptors()
}
}

@ -0,0 +1,13 @@
//
// PresentationContext.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import Foundation
enum PresentationContext {
case agenda
case organizer
}

@ -0,0 +1,13 @@
//
// Screen.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import Foundation
enum Screen: String, Codable {
case inscription
case groupStage
}

@ -7,16 +7,6 @@
import SwiftUI
enum Screen: String, Codable {
case inscription
case groupStage
}
enum PresentationContext {
case agenda
case organizer
}
struct TournamentView: View {
@State var tournament: Tournament
var presentationContext: PresentationContext = .agenda
@ -66,7 +56,3 @@ struct TournamentView: View {
}
}
}
//
//#Preview {
// TournamentView(tournament: Tournament(name: "", club_id: "", category: 0, playerCount: 0))
//}

@ -0,0 +1,55 @@
//
// DeferredViewModifier.swift
// PadelClub
//
// Created by Razmig Sarkissian on 02/03/2024.
//
import SwiftUI
/// Defers the rendering of a view for the given period.
///
/// For example:
///
/// ```swift
/// Text("Hello, world!")
/// .deferredRendering(for: .seconds(5))
/// ```
///
/// will not display the text "Hello, world!" until five seconds after the
/// view is initially rendered. If the view is destroyed within the delay,
/// it never renders.
///
/// This is based on code xwritten by Yonat and Charlton Provatas on
/// Stack Overflow, see https://stackoverflow.com/a/74765430/1558022
///
private struct DeferredViewModifier: ViewModifier {
let delay: DispatchTimeInterval
func body(content: Content) -> some View {
_content(content)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.shouldHide = true
}
}
}
@ViewBuilder
private func _content(_ content: Content) -> some View {
if shouldHide == false {
content
} else {
content.hidden()
}
}
@State private var shouldHide = false
}
extension View {
func deferredRendering(for delay: DispatchTimeInterval) -> some View {
modifier(DeferredViewModifier(delay: delay))
}
}
Loading…
Cancel
Save