From 53756b3c588a4b4a6c8964a89476d1b3f45b4ed2 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 26 Mar 2024 07:14:04 +0100 Subject: [PATCH] handle updates --- PadelClub.xcodeproj/project.pbxproj | 204 ++++++++++++- PadelClub/Data/DataStore.swift | 18 +- PadelClub/Data/GroupStage.swift | 8 +- PadelClub/Data/MockData.swift | 15 +- PadelClub/Data/PlayerRegistration.swift | 119 +++++++- PadelClub/Data/TeamRegistration.swift | 81 ++++- PadelClub/Data/Tournament.swift | 173 ++++++++--- .../Extensions/Sequence+Extensions.swift | 17 ++ PadelClub/Extensions/String+Extensions.swift | 4 + PadelClub/Extensions/URL+Extensions.swift | 39 +++ PadelClub/Info.plist | 22 ++ PadelClub/Manager/FileImportManager.swift | 242 +++++++++++++++ .../Manager/Network/NetworkManager.swift | 3 +- PadelClub/Manager/SourceFileManager.swift | 91 ++++++ PadelClub/Manager/SwiftParser.swift | 30 +- PadelClub/Manager/Tips.swift | 4 + PadelClub/Views/Components/StepperView.swift | 2 +- PadelClub/Views/ContentView.swift | 26 +- PadelClub/Views/Event/EventCreationView.swift | 8 +- .../Views/GroupStage/GroupStageView.swift | 7 +- PadelClub/Views/Navigation/MainView.swift | 55 +--- PadelClub/Views/Team/TeamDetailView.swift | 2 +- .../Views/Tournament/FileImportView.swift | 285 +++++++++++++++++- .../Components/InscriptionTipsView.swift | 4 +- .../TournamentFieldsManagerView.swift | 2 +- .../Components/UpdateSourceRankDateView.swift | 80 ++++- .../Screen/InscriptionManagerView.swift | 207 +++++++++++-- .../Views/Tournament/TournamentView.swift | 2 +- 28 files changed, 1541 insertions(+), 209 deletions(-) create mode 100644 PadelClub/Info.plist diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 086901c..5bf1cab 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -37,6 +37,51 @@ FF089EB82BB00ABF00F0AEC7 /* InscriptionTipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB72BB00ABF00F0AEC7 /* InscriptionTipsView.swift */; }; FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */; }; FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */; }; + FF089EBF2BB0B14600F0AEC7 /* FileImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBE2BB0B14600F0AEC7 /* FileImportView.swift */; }; + FF0EC5202BB16F680056B6D1 /* SwiftParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */; }; + FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */; }; + FF0EC54E2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5382BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv */; }; + FF0EC54F2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-08-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5482BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-08-2022.csv */; }; + FF0EC5502BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-12-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5352BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-12-2022.csv */; }; + FF0EC5512BB195E20056B6D1 /* CLASSEMENT PADEL DAMES-07-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5232BB195CA0056B6D1 /* CLASSEMENT PADEL DAMES-07-2023.csv */; }; + FF0EC5522BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-02-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5412BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-02-2023.csv */; }; + FF0EC5532BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-09-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC53D2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-09-2022.csv */; }; + FF0EC5542BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-04-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC53A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-04-2023.csv */; }; + FF0EC5552BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_2-08-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5282BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_2-08-2023.csv */; }; + FF0EC5562BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-10-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC53E2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-10-2022.csv */; }; + FF0EC5572BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-10-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5332BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-10-2022.csv */; }; + FF0EC5582BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-3-05-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5422BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-3-05-2023.csv */; }; + FF0EC5592BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-03-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC52D2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-03-2023.csv */; }; + FF0EC55A2BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_2-07-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5272BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_2-07-2023.csv */; }; + FF0EC55B2BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-01-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC52B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-01-2023.csv */; }; + FF0EC55C2BB195E20056B6D1 /* CLASSEMENT PADEL DAMES-08-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5242BB195CA0056B6D1 /* CLASSEMENT PADEL DAMES-08-2023.csv */; }; + FF0EC55D2BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-04-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC52E2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-04-2023.csv */; }; + FF0EC55E2BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-05-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC52F2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-05-2023.csv */; }; + FF0EC55F2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-03-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5392BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-03-2023.csv */; }; + FF0EC5602BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_3-08-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC52A2BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_3-08-2023.csv */; }; + FF0EC5612BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-05-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5462BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-05-2023.csv */; }; + FF0EC5622BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-05-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC53B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-05-2023.csv */; }; + FF0EC5632BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-06-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC53C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-06-2023.csv */; }; + FF0EC5642BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-11-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC53F2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-11-2022.csv */; }; + FF0EC5652BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-12-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5402BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-12-2022.csv */; }; + FF0EC5662BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-02-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC52C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-02-2023.csv */; }; + FF0EC5672BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-03-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5442BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-03-2023.csv */; }; + FF0EC5682BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-06-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5302BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-06-2023.csv */; }; + FF0EC5692BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-11-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC54B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-11-2022.csv */; }; + FF0EC56A2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-12-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC54C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-12-2022.csv */; }; + FF0EC56B2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-06-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5472BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-06-2023.csv */; }; + FF0EC56C2BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_1-07-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5252BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_1-07-2023.csv */; }; + FF0EC56D2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-3-06-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5432BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-3-06-2023.csv */; }; + FF0EC56E2BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_1-08-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5262BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_1-08-2023.csv */; }; + FF0EC56F2BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-09-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5322BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-09-2022.csv */; }; + FF0EC5702BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-08-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5312BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-08-2022.csv */; }; + FF0EC5712BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-01-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5362BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-01-2023.csv */; }; + FF0EC5722BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-09-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5492BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-09-2022.csv */; }; + FF0EC5732BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_3-07-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5292BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_3-07-2023.csv */; }; + FF0EC5742BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-01-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5372BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-01-2023.csv */; }; + FF0EC5752BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-11-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5342BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-11-2022.csv */; }; + FF0EC5762BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5452BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv */; }; + FF0EC5772BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC54A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv */; }; FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5502BAB351300FD8220 /* ClubDetailView.swift */; }; FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5522BAB354A00FD8220 /* MockData.swift */; }; FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5542BAB36DD00FD8220 /* CreateClubView.swift */; }; @@ -104,6 +149,8 @@ FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */; }; FF967D0D2BAF3EB300A9A3BD /* MatchDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */; }; FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */; }; + FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; + FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; }; FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; @@ -114,7 +161,6 @@ 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 */; }; @@ -201,6 +247,51 @@ FF089EB72BB00ABF00F0AEC7 /* InscriptionTipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionTipsView.swift; sourceTree = ""; }; FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPopoverView.swift; sourceTree = ""; }; FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; }; + FF089EBE2BB0B14600F0AEC7 /* FileImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileImportView.swift; sourceTree = ""; }; + FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftParser.swift; sourceTree = ""; }; + FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSourceRankDateView.swift; sourceTree = ""; }; + FF0EC5232BB195CA0056B6D1 /* CLASSEMENT PADEL DAMES-07-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT PADEL DAMES-07-2023.csv"; sourceTree = ""; }; + FF0EC5242BB195CA0056B6D1 /* CLASSEMENT PADEL DAMES-08-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT PADEL DAMES-08-2023.csv"; sourceTree = ""; }; + FF0EC5252BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_1-07-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT PADEL MESSIEURS_1-07-2023.csv"; sourceTree = ""; }; + FF0EC5262BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_1-08-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT PADEL MESSIEURS_1-08-2023.csv"; sourceTree = ""; }; + FF0EC5272BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_2-07-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT PADEL MESSIEURS_2-07-2023.csv"; sourceTree = ""; }; + FF0EC5282BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_2-08-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT PADEL MESSIEURS_2-08-2023.csv"; sourceTree = ""; }; + FF0EC5292BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_3-07-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT PADEL MESSIEURS_3-07-2023.csv"; sourceTree = ""; }; + FF0EC52A2BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_3-08-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT PADEL MESSIEURS_3-08-2023.csv"; sourceTree = ""; }; + FF0EC52B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-01-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-DAMES-01-2023.csv"; sourceTree = ""; }; + FF0EC52C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-02-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-DAMES-02-2023.csv"; sourceTree = ""; }; + FF0EC52D2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-03-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-DAMES-03-2023.csv"; sourceTree = ""; }; + FF0EC52E2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-04-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-DAMES-04-2023.csv"; sourceTree = ""; }; + FF0EC52F2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-05-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-DAMES-05-2023.csv"; sourceTree = ""; }; + FF0EC5302BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-06-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-DAMES-06-2023.csv"; sourceTree = ""; }; + FF0EC5312BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-08-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-DAMES-08-2022.csv"; sourceTree = ""; }; + FF0EC5322BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-09-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-DAMES-09-2022.csv"; sourceTree = ""; }; + FF0EC5332BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-10-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-DAMES-10-2022.csv"; sourceTree = ""; }; + FF0EC5342BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-11-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-DAMES-11-2022.csv"; sourceTree = ""; }; + FF0EC5352BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-12-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-DAMES-12-2022.csv"; sourceTree = ""; }; + FF0EC5362BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-01-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-01-2023.csv"; sourceTree = ""; }; + FF0EC5372BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-01-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-2-01-2023.csv"; sourceTree = ""; }; + FF0EC5382BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv"; sourceTree = ""; }; + FF0EC5392BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-03-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-2-03-2023.csv"; sourceTree = ""; }; + FF0EC53A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-04-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-2-04-2023.csv"; sourceTree = ""; }; + FF0EC53B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-05-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-2-05-2023.csv"; sourceTree = ""; }; + FF0EC53C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-06-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-2-06-2023.csv"; sourceTree = ""; }; + FF0EC53D2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-09-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-2-09-2022.csv"; sourceTree = ""; }; + FF0EC53E2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-10-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-2-10-2022.csv"; sourceTree = ""; }; + FF0EC53F2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-11-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-2-11-2022.csv"; sourceTree = ""; }; + FF0EC5402BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-12-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-2-12-2022.csv"; sourceTree = ""; }; + FF0EC5412BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-02-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-02-2023.csv"; sourceTree = ""; }; + FF0EC5422BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-3-05-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-3-05-2023.csv"; sourceTree = ""; }; + FF0EC5432BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-3-06-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-3-06-2023.csv"; sourceTree = ""; }; + FF0EC5442BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-03-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-03-2023.csv"; sourceTree = ""; }; + FF0EC5452BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-04-2023.csv"; sourceTree = ""; }; + FF0EC5462BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-05-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-05-2023.csv"; sourceTree = ""; }; + FF0EC5472BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-06-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-06-2023.csv"; sourceTree = ""; }; + FF0EC5482BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-08-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-08-2022.csv"; sourceTree = ""; }; + FF0EC5492BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-09-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-09-2022.csv"; sourceTree = ""; }; + FF0EC54A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-10-2022.csv"; sourceTree = ""; }; + FF0EC54B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-11-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-11-2022.csv"; sourceTree = ""; }; + FF0EC54C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-12-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-12-2022.csv"; sourceTree = ""; }; FF1DC5502BAB351300FD8220 /* ClubDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubDetailView.swift; sourceTree = ""; }; FF1DC5522BAB354A00FD8220 /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = ""; }; FF1DC5542BAB36DD00FD8220 /* CreateClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateClubView.swift; sourceTree = ""; }; @@ -266,6 +357,9 @@ FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamPickerView.swift; sourceTree = ""; }; FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchDateView.swift; sourceTree = ""; }; FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBlockView.swift; sourceTree = ""; }; + FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = ""; }; + FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = ""; }; + FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = ""; }; FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; @@ -277,7 +371,6 @@ FFD784032B91C280000F62A6 /* EmptyActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyActivityView.swift; sourceTree = ""; }; FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = ""; }; FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = ""; }; - FFF8ACD12B9238C3008466FA /* FileImportManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = ""; }; FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = ""; }; FFF8ACD52B923960008466FA /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; FFF8ACD82B923F3C008466FA /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; @@ -334,6 +427,7 @@ C425D3FF2B6D249D002A7B48 /* PadelClub */ = { isa = PBXGroup; children = ( + FFA6D78A2BB0BEB3003A31F3 /* Info.plist */, C425D44E2B6D24E1002A7B48 /* LeStorage.xcodeproj */, C425D4002B6D249D002A7B48 /* PadelClubApp.swift */, FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */, @@ -343,6 +437,7 @@ FFF8ACD02B9238A2008466FA /* Manager */, FFF8ACD72B923F26008466FA /* Extensions */, C425D4042B6D249E002A7B48 /* Assets.xcassets */, + FF0EC54D2BB195CA0056B6D1 /* CSV */, C425D4062B6D249E002A7B48 /* Preview Content */, ); path = PadelClub; @@ -494,6 +589,55 @@ path = Player; sourceTree = ""; }; + FF0EC54D2BB195CA0056B6D1 /* CSV */ = { + isa = PBXGroup; + children = ( + FF0EC5232BB195CA0056B6D1 /* CLASSEMENT PADEL DAMES-07-2023.csv */, + FF0EC5242BB195CA0056B6D1 /* CLASSEMENT PADEL DAMES-08-2023.csv */, + FF0EC5252BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_1-07-2023.csv */, + FF0EC5262BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_1-08-2023.csv */, + FF0EC5272BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_2-07-2023.csv */, + FF0EC5282BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_2-08-2023.csv */, + FF0EC5292BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_3-07-2023.csv */, + FF0EC52A2BB195CA0056B6D1 /* CLASSEMENT PADEL MESSIEURS_3-08-2023.csv */, + FF0EC52B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-01-2023.csv */, + FF0EC52C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-02-2023.csv */, + FF0EC52D2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-03-2023.csv */, + FF0EC52E2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-04-2023.csv */, + FF0EC52F2BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-05-2023.csv */, + FF0EC5302BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-06-2023.csv */, + FF0EC5312BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-08-2022.csv */, + FF0EC5322BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-09-2022.csv */, + FF0EC5332BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-10-2022.csv */, + FF0EC5342BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-11-2022.csv */, + FF0EC5352BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-12-2022.csv */, + FF0EC5362BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-01-2023.csv */, + FF0EC5372BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-01-2023.csv */, + FF0EC5382BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv */, + FF0EC5392BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-03-2023.csv */, + FF0EC53A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-04-2023.csv */, + FF0EC53B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-05-2023.csv */, + FF0EC53C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-06-2023.csv */, + FF0EC53D2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-09-2022.csv */, + FF0EC53E2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-10-2022.csv */, + FF0EC53F2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-11-2022.csv */, + FF0EC5402BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-12-2022.csv */, + FF0EC5412BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-02-2023.csv */, + FF0EC5422BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-3-05-2023.csv */, + FF0EC5432BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-3-06-2023.csv */, + FF0EC5442BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-03-2023.csv */, + FF0EC5452BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv */, + FF0EC5462BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-05-2023.csv */, + FF0EC5472BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-06-2023.csv */, + FF0EC5482BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-08-2022.csv */, + FF0EC5492BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-09-2022.csv */, + FF0EC54A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv */, + FF0EC54B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-11-2022.csv */, + FF0EC54C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-12-2022.csv */, + ); + path = CSV; + sourceTree = ""; + }; FF1DC54D2BAB34FA00FD8220 /* Club */ = { isa = PBXGroup; children = ( @@ -525,6 +669,7 @@ FF70916B2B91005400AB08DA /* TournamentView.swift */, FF8F26402BADFC8700650388 /* TournamentInitView.swift */, FF967CF52BAED51600A9A3BD /* TournamentRunningView.swift */, + FF089EBE2BB0B14600F0AEC7 /* FileImportView.swift */, FF3F74F92B91A018004CFE0E /* Screen */, FF3F74F82B919FB2004CFE0E /* Shared */, ); @@ -645,6 +790,7 @@ FF8F26482BAE0B4100650388 /* TournamentFormatSelectionView.swift */, FF8F26492BAE0B4100650388 /* TournamentLevelPickerView.swift */, FF089EB72BB00ABF00F0AEC7 /* InscriptionTipsView.swift */, + FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */, ); path = Components; sourceTree = ""; @@ -705,11 +851,13 @@ isa = PBXGroup; children = ( FF1DC5582BAB767000FD8220 /* Tips.swift */, + FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */, - FFF8ACD12B9238C3008466FA /* FileImportManager.swift */, FFF8ACD32B92392C008466FA /* SourceFileManager.swift */, FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */, FF8F26352BAD523300650388 /* PadelRule.swift */, + FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */, + FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */, FF6EC9072B947A1E00EA7F5A /* Network */, ); path = Manager; @@ -857,6 +1005,48 @@ buildActionMask = 2147483647; files = ( C425D4082B6D249E002A7B48 /* Preview Assets.xcassets in Resources */, + FF0EC54E2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv in Resources */, + FF0EC54F2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-08-2022.csv in Resources */, + FF0EC5502BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-12-2022.csv in Resources */, + FF0EC5512BB195E20056B6D1 /* CLASSEMENT PADEL DAMES-07-2023.csv in Resources */, + FF0EC5522BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-02-2023.csv in Resources */, + FF0EC5532BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-09-2022.csv in Resources */, + FF0EC5542BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-04-2023.csv in Resources */, + FF0EC5552BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_2-08-2023.csv in Resources */, + FF0EC5562BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-10-2022.csv in Resources */, + FF0EC5572BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-10-2022.csv in Resources */, + FF0EC5582BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-3-05-2023.csv in Resources */, + FF0EC5592BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-03-2023.csv in Resources */, + FF0EC55A2BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_2-07-2023.csv in Resources */, + FF0EC55B2BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-01-2023.csv in Resources */, + FF0EC55C2BB195E20056B6D1 /* CLASSEMENT PADEL DAMES-08-2023.csv in Resources */, + FF0EC55D2BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-04-2023.csv in Resources */, + FF0EC55E2BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-05-2023.csv in Resources */, + FF0EC55F2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-03-2023.csv in Resources */, + FF0EC5602BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_3-08-2023.csv in Resources */, + FF0EC5612BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-05-2023.csv in Resources */, + FF0EC5622BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-05-2023.csv in Resources */, + FF0EC5632BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-06-2023.csv in Resources */, + FF0EC5642BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-11-2022.csv in Resources */, + FF0EC5652BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-12-2022.csv in Resources */, + FF0EC5662BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-02-2023.csv in Resources */, + FF0EC5672BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-03-2023.csv in Resources */, + FF0EC5682BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-06-2023.csv in Resources */, + FF0EC5692BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-11-2022.csv in Resources */, + FF0EC56A2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-12-2022.csv in Resources */, + FF0EC56B2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-06-2023.csv in Resources */, + FF0EC56C2BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_1-07-2023.csv in Resources */, + FF0EC56D2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-3-06-2023.csv in Resources */, + FF0EC56E2BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_1-08-2023.csv in Resources */, + FF0EC56F2BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-09-2022.csv in Resources */, + FF0EC5702BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-08-2022.csv in Resources */, + FF0EC5712BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-01-2023.csv in Resources */, + FF0EC5722BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-09-2022.csv in Resources */, + FF0EC5732BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_3-07-2023.csv in Resources */, + FF0EC5742BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-01-2023.csv in Resources */, + FF0EC5752BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-11-2022.csv in Resources */, + FF0EC5762BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv in Resources */, + FF0EC5772BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv in Resources */, C425D4052B6D249E002A7B48 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -885,6 +1075,7 @@ C4A47D872B7BA36D00ADC637 /* UserCreationView.swift in Sources */, FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */, FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */, + FF089EBF2BB0B14600F0AEC7 /* FileImportView.swift in Sources */, C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */, FF8F26412BADFC8700650388 /* TournamentInitView.swift in Sources */, C4A47D8A2B7BBB6500ADC637 /* SubscriptionView.swift in Sources */, @@ -899,6 +1090,7 @@ FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */, FFD783FD2B91B9ED000F62A6 /* AgendaDestinationPickerView.swift in Sources */, FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */, + FF0EC5202BB16F680056B6D1 /* SwiftParser.swift in Sources */, C4A47DA92B85F82100ADC637 /* ChangePasswordView.swift in Sources */, FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */, FF8F263B2BAD528600650388 /* EventCreationView.swift in Sources */, @@ -939,6 +1131,7 @@ FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */, FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */, FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */, + FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, @@ -948,6 +1141,7 @@ FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */, + FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, @@ -960,7 +1154,6 @@ FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */, C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */, C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */, - FFF8ACD22B9238C3008466FA /* FileImportManager.swift in Sources */, FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */, FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */, FF089EB82BB00ABF00F0AEC7 /* InscriptionTipsView.swift in Sources */, @@ -986,6 +1179,7 @@ FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */, FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */, + FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */, C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1153,6 +1347,7 @@ DEVELOPMENT_TEAM = 526E96RFNP; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -1183,6 +1378,7 @@ DEVELOPMENT_TEAM = 526E96RFNP; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index fa6db7c..55c3f80 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -43,15 +43,15 @@ class DataStore: ObservableObject { // store.addMigration(Migration(version: 2)) // store.addMigration(Migration(version: 3)) - self.clubs = store.registerCollection(synchronized: false) - self.tournaments = store.registerCollection(synchronized: false) - self.events = store.registerCollection(synchronized: false) - self.groupStages = store.registerCollection(synchronized: false) - self.teamScores = store.registerCollection(synchronized: false) - self.teamRegistrations = store.registerCollection(synchronized: false) - self.playerRegistrations = store.registerCollection(synchronized: false) - self.rounds = store.registerCollection(synchronized: false) - self.matches = store.registerCollection(synchronized: false) + self.clubs = store.registerCollection(synchronized: false, indexed: true) + self.tournaments = store.registerCollection(synchronized: false, indexed: true) + self.events = store.registerCollection(synchronized: false, indexed: true) + self.groupStages = store.registerCollection(synchronized: false, indexed: true) + self.teamScores = store.registerCollection(synchronized: false, indexed: true) + self.teamRegistrations = store.registerCollection(synchronized: false, indexed: true) + self.playerRegistrations = store.registerCollection(synchronized: false, indexed: true) + self.rounds = store.registerCollection(synchronized: false, indexed: true) + self.matches = store.registerCollection(synchronized: false, indexed: true) 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) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index be16b6b..b06f625 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -36,6 +36,10 @@ class GroupStage: ModelObject, Storable { self.startDate = startDate } + func teamsAt(_ index: Int) -> TeamRegistration? { + teams().first(where: { $0.groupStagePosition == index }) + } + func tournamentObject() -> Tournament? { Store.main.findById(tournament) } @@ -75,7 +79,7 @@ class GroupStage: ModelObject, Storable { _matches.append(newMatch) } - try? DataStore.shared.matches.append(contentOfs: _matches) + try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) } func removeMatches() { @@ -90,7 +94,7 @@ class GroupStage: ModelObject, Storable { Store.main.filter { $0.groupStage == self.id } } - var teams: [TeamRegistration] { + func teams() -> [TeamRegistration] { Store.main.filter { $0.groupStage == self.id } } diff --git a/PadelClub/Data/MockData.swift b/PadelClub/Data/MockData.swift index 84cfb85..9ee06bb 100644 --- a/PadelClub/Data/MockData.swift +++ b/PadelClub/Data/MockData.swift @@ -23,7 +23,20 @@ extension Tournament { } static func newEmptyInstance() -> Tournament { - Tournament(groupStageSortMode: .snake, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior) + let lastDataSource: String? = UserDefaults.standard.string(forKey: "lastDataSource") + let lastDataSourceMaleUnranked: Int = UserDefaults.standard.integer(forKey: "lastDataSourceMaleUnranked") + let lastDataSourceFemaleUnranked: Int = UserDefaults.standard.integer(forKey: "lastDataSourceFemaleUnranked") + + var _mostRecentDateAvailable: Date? { + guard let lastDataSource else { return nil } + return URL.importDateFormatter.date(from: lastDataSource) + } + + let maleUnrankedValue : Int? = lastDataSourceMaleUnranked == 0 ? nil : lastDataSourceMaleUnranked + let femaleUnrankedValue : Int? = lastDataSourceFemaleUnranked == 0 ? nil : lastDataSourceMaleUnranked + let rankSourceDate = _mostRecentDateAvailable + + return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior, maleUnrankedValue: maleUnrankedValue, femaleUnrankedValue: femaleUnrankedValue) } } diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 2a65c7b..22945ef 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -28,11 +28,12 @@ class PlayerRegistration: ModelObject, Storable { var ligueName: String? var assimilation: String? -// var phoneNumber: String? -// var email: String? -// var birthDate: Date? -// var club: String? + var phoneNumber: String? + var email: String? + var birthdate: String? + var weight: Int = 0 + internal init(teamRegistration: String = "", firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, registrationType: Int? = nil, registrationDate: Date? = nil, sex: Int) { self.teamRegistration = teamRegistration self.firstName = firstName @@ -58,6 +59,39 @@ class PlayerRegistration: ModelObject, Storable { self.assimilation = importedPlayer.assimilation } + internal init(federalData: [String], sex: Int, sexUnknown: Bool) { + lastName = federalData[0] + firstName = federalData[1] + birthdate = federalData[2] + licenceId = federalData[3] + clubName = federalData[4] + rank = Int(federalData[5]) + email = federalData[6] + phoneNumber = federalData[7] +// manuallyCreated = false + if sexUnknown { + if sex == 1 && FileImportManager.shared.foundInWomenData(license: federalData[3]) { + self.sex = 0 + } else if FileImportManager.shared.foundInMenData(license: federalData[3]) { + self.sex = 1 + } else { + self.sex = -1 + } + } else { + self.sex = sex + } + } + + + func contains(_ searchField: String) -> Bool { + firstName.localizedCaseInsensitiveContains(searchField) || lastName.localizedCaseInsensitiveContains(searchField) + } + + func isSameAs(_ player: PlayerRegistration) -> Bool { + firstName.localizedCaseInsensitiveCompare(player.firstName) == .orderedSame && + lastName.localizedCaseInsensitiveCompare(player.lastName) == .orderedSame + } + func tournament() -> Tournament? { guard let tournament = team()?.tournament else { return nil } return Store.main.findById(tournament) @@ -87,22 +121,78 @@ class PlayerRegistration: ModelObject, Storable { func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String { if let rank, rank > 0 { - return rank.ordinalFormatted() + return rank.formatted() } else { return "non classé" + (isMalePlayer() ? "" : "e") } } + + func getRank() -> Int { + weight + } + + @MainActor + func updateRank(from sources: [CSVParser], lastRank: Int) async throws { + if let value = try await history(from: sources)?.rank { + rank = value + } else { + rank = lastRank + } + } + + func history(from sources: [CSVParser]) async throws -> Line? { + guard let license = licenceId?.strippedLicense else { + return try await historyFromName(from: sources) + } + + return await withTaskGroup(of: Line?.self) { group in + for source in sources.filter({ $0.maleData == isMalePlayer() }) { + group.addTask { + guard !Task.isCancelled else { print("Cancelled"); return nil } - var computedRank: Int { - rank ?? tournament()?.unrankValue(for: isMalePlayer()) ?? Int.max + return try? await source.first(where: { line in + line.rawValue.contains(";\(license);") + }) + } + } + + if let first = await group.first(where: { $0 != nil }) { + group.cancelAll() + return first + } else { + return nil + } + } } - func rank(for tournamentCategory: TournamentCategory, manMax: Int, womanMax: Int) -> Int { - switch tournamentCategory { + func historyFromName(from sources: [CSVParser]) async throws -> Line? { + return await withTaskGroup(of: Line?.self) { group in + for source in sources.filter({ $0.maleData == isMalePlayer() }) { + group.addTask { [lastName, firstName] in + guard !Task.isCancelled else { print("Cancelled"); return nil } + + return try? await source.first(where: { line in + line.rawValue.canonicalVersionWithPunctuation.contains(";\(lastName.canonicalVersionWithPunctuation);\(firstName.canonicalVersionWithPunctuation);") + }) + } + } + + if let first = await group.first(where: { $0 != nil }) { + group.cancelAll() + return first + } else { + return nil + } + } + } + + func setWeight(in tournament: Tournament) { + let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 100_000 + switch tournament.tournamentCategory { case .men: - return isMalePlayer() ? computedRank : computedRank + PlayerRegistration.addon(for: computedRank, manMax: manMax, womanMax: womanMax) - case .women, .mix: - return computedRank + weight = isMalePlayer() ? currentRank : currentRank + PlayerRegistration.addon(for: currentRank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0) + default: + weight = currentRank } } @@ -125,6 +215,11 @@ class PlayerRegistration: ModelObject, Storable { case _clubName = "clubName" case _ligueName = "ligueName" case _assimilation = "assimilation" + case _birthdate = "birthdate" + case _phoneNumber = "phoneNumber" + case _email = "email" + case _weight = "weight" + } enum PaymentType: Int, CaseIterable, Identifiable { diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 7b316de..f7bf45d 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -24,8 +24,14 @@ class TeamRegistration: ModelObject, Storable { var sourceValue: String? var logo: String? var name: String? - - internal init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil) { + + var walkOut: Bool = false + var wildCardBracket: Bool = false + var wildCardGroupStage: Bool = false + var category: Int? + var weight: Int = 0 + + internal init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, category: Int? = nil) { self.tournament = tournament self.groupStage = groupStage self.registrationDate = registrationDate @@ -37,6 +43,44 @@ class TeamRegistration: ModelObject, Storable { self.sourceValue = sourceValue self.logo = logo self.name = name + self.category = category + } + + var tournamentCategory: TournamentCategory { + get { + TournamentCategory(rawValue: category ?? 0) ?? .men + } + set { + category = newValue.rawValue + } + } + + func teamLabel(_ displayStyle: DisplayStyle = .wide) -> String { + unsortedPlayers().map { $0.playerLabel(displayStyle) }.joined(separator: " & ") + } + + func formattedSeed(in teams: [TeamRegistration]) -> String { + if let index = teams.firstIndex(where: { $0.id == id }) { + return "#\(index + 1)" + } else { + return "###" + } + } + + func contains(_ searchField: String) -> Bool { + unsortedPlayers().anySatisfy({ $0.contains(searchField) }) || self.name?.localizedCaseInsensitiveContains(searchField) == true + } + + func includes(_ players: [PlayerRegistration]) -> Bool { + players.allSatisfy { player in + includes(player) + } + } + + func includes(_ player: PlayerRegistration) -> Bool { + unsortedPlayers().anySatisfy { _player in + _player.isSameAs(player) + } } var computedPosition: Int { @@ -44,12 +88,14 @@ class TeamRegistration: ModelObject, Storable { } func updatePlayers(_ players: Set) { - self.players().forEach { player in + self.unsortedPlayers().forEach { player in if players.contains(player) == false { try? DataStore.shared.playerRegistrations.delete(instance: player) } } - + + setWeight(from: Array(players)) + players.forEach { player in player.teamRegistration = id try? DataStore.shared.playerRegistrations.addOrUpdate(instance: player) @@ -68,7 +114,7 @@ class TeamRegistration: ModelObject, Storable { Store.main.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in let predicates: [AreInIncreasingOrder] = [ { $0.sex < $1.sex }, - { $0.computedRank < $1.computedRank }, + { $0.rank ?? 0 < $1.rank ?? 0 }, { $0.lastName < $1.lastName}, { $0.firstName < $1.firstName } ] @@ -85,8 +131,13 @@ class TeamRegistration: ModelObject, Storable { } } - func computedRank() -> Int { - (players().prefix(significantPlayerCount()).map { $0.computedRank } + missing().map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount()).reduce(0,+) + func unsortedPlayers() -> [PlayerRegistration] { + Store.main.filter { $0.teamRegistration == self.id } + } + + func setWeight(from players: [PlayerRegistration]) { + let significantPlayerCount = significantPlayerCount() + weight = (players.prefix(significantPlayerCount).map { $0.weight } + missingPlayerType().map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+) } func significantPlayerCount() -> Int { @@ -94,7 +145,6 @@ class TeamRegistration: ModelObject, Storable { } func mandatoryPlayerType() -> [Int] { - guard let tournamentCategory = tournamentObject()?.tournamentCategory else { return [] } switch tournamentCategory { case .mix: return [0, 1] @@ -105,8 +155,10 @@ class TeamRegistration: ModelObject, Storable { } } - func missing() -> [Int] { - let s = players().map { $0.sex } + func missingPlayerType() -> [Int] { + let players = unsortedPlayers() + if players.count < 2 { return [] } + let s = players.map { $0.sex } var missing = mandatoryPlayerType() s.forEach { i in if let index = missing.firstIndex(of: i) { @@ -117,7 +169,7 @@ class TeamRegistration: ModelObject, Storable { } func unrankValue(for malePlayer: Bool) -> Int { - tournamentObject()?.unrankValue(for: malePlayer) ?? Int.max + tournamentObject()?.unrankValue(for: malePlayer) ?? 100_000 } func tournamentObject() -> Tournament? { @@ -125,7 +177,7 @@ class TeamRegistration: ModelObject, Storable { } override func deleteDependencies() throws { - try Store.main.deleteDependencies(items: self.players()) + try Store.main.deleteDependencies(items: self.unsortedPlayers()) } enum CodingKeys: String, CodingKey { @@ -141,5 +193,10 @@ class TeamRegistration: ModelObject, Storable { case _sourceValue = "sourceValue" case _logo = "logo" case _name = "name" + case _wildCardBracket = "wildCardBracket" + case _wildCardGroupStage = "wildCardGroupStage" + case _category = "category" + case _weight = "weight" + case _walkOut = "walkOut" } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 23cfb3c..7d9cb50 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -99,22 +99,54 @@ class Tournament : ModelObject, Storable { Store.main.filter { $0.tournament == self.id }.sorted(by: \.index) } + func unsortedTeams() -> [TeamRegistration] { + Store.main.filter { $0.tournament == self.id } + } + + typealias TeamRegistrationCompare = (TeamRegistration, TeamRegistration) -> Bool + func teams() -> [TeamRegistration] { - Store.main.filter { $0.tournament == self.id }.sorted { - if $0.computedRank() == $1.computedRank() { - return $0.registrationDate ?? .distantPast < $1.registrationDate ?? .distantPast - } else { - return $0.computedRank() < $1.computedRank() + Store.main.filter { $0.tournament == self.id } + .sorted { (lhs, rhs) in + let predicates: [TeamRegistrationCompare] = [ + { $0.weight < $1.weight }, + { $0.registrationDate ?? .distantPast < $1.registrationDate ?? .distantPast }, + ] + + for predicate in predicates { + if !predicate(lhs, rhs) && !predicate(rhs, lhs) { + continue + } + + return predicate(lhs, rhs) + } + + return false + } + } + + func duplicates() -> [PlayerRegistration] { + var duplicates = [PlayerRegistration]() + let players = unsortedPlayers() + Set(players.compactMap({ $0.licenceId })).forEach { licenceId in + let found = players.filter({ $0.licenceId == licenceId }) + if found.count > 1 { + duplicates.append(found.first!) } } + return duplicates } + func unsortedPlayers() -> [PlayerRegistration] { + unsortedTeams().flatMap { $0.unsortedPlayers() } + } + func players() -> [PlayerRegistration] { - teams().flatMap { $0.players() } + unsortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.weight) } - + func femalePlayers() -> [PlayerRegistration] { - players().filter({ $0.isMalePlayer() == false }) + unsortedPlayers().filter({ $0.isMalePlayer() == false }) } func unrankValue(for malePlayer: Bool) -> Int? { @@ -135,11 +167,61 @@ class Tournament : ModelObject, Storable { func significantPlayerCount() -> Int { 2 } + + func importTeams(_ teams: [FileImportManager.TeamHolder], keepPreviousData: Bool = false) { + teams.forEach { team in + addTeam(Set([team.playerOne, team.playerTwo])) + } + } + + func updateWeights() { + let teams = self.unsortedTeams() + teams.forEach { team in + let players = team.unsortedPlayers() + players.forEach { $0.setWeight(in: self) } + team.setWeight(from: players) + try? DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: players) + } + try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teams) + } + + func updateRank(to newDate: Date?) async throws { + guard let newDate else { return } + rankSourceDate = newDate + + let maleDataURLs = SourceFile.allFiles(true).filter { $0.dateFromPath == newDate } + let femaleDataURLs = SourceFile.allFiles(false).filter { $0.dateFromPath == newDate } + + let lastRankWoman = femaleDataURLs.compactMap { $0.getUnrankedValue() }.first + let lastRankMan = maleDataURLs.compactMap { $0.getUnrankedValue() }.sorted().last + + let dataURLs = maleDataURLs + femaleDataURLs + let sources = dataURLs.map { CSVParser(url: $0) } + + await unsortedPlayers().concurrentForEach { player in + try? await player.updateRank(from: sources, lastRank: (player.sex == 0 ? lastRankWoman : lastRankMan) ?? 0) + } + + await MainActor.run { + self.maleUnrankedValue = lastRankMan + self.femaleUnrankedValue = lastRankWoman + +// if inscriptionClosed == false { +// orderedEntries.forEach { entrant in +// entrant.initialRank = entrant.updatedRank +// } +// } + } + } func missingUnrankedValue() -> Bool { maleUnrankedValue == nil || femaleUnrankedValue == nil } + func findTeam(_ players: [PlayerRegistration]) -> TeamRegistration? { + unsortedTeams().first(where: { $0.includes(players) }) + } + func title(_ displayStyle: DisplayStyle = .wide) -> String { [tournamentLevel.localizedLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), name].compactMap({ $0 }).joined(separator: " ") } @@ -163,7 +245,7 @@ class Tournament : ModelObject, Storable { func qualifiedTeams() -> [TeamRegistration] { - teams().filter({ $0.qualified() }) + unsortedTeams().filter({ $0.qualified() }) } func moreQualifiedToDraw() -> Int { @@ -173,7 +255,7 @@ class Tournament : ModelObject, Storable { func missingQualifiedFromGroupStages() -> [TeamRegistration] { if groupStageAdditionalQualified > 0 { return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in - groupStage.teams[qualifiedPerGroupStage] + groupStage.teams()[qualifiedPerGroupStage] } .filter({ $0.qualified() == false }) } else { @@ -228,10 +310,10 @@ class Tournament : ModelObject, Storable { _groupStages.append(groupStage) } - try? DataStore.shared.groupStages.append(contentOfs: _groupStages) + try? DataStore.shared.groupStages.addOrUpdate(contentOfs: _groupStages) groupStages().forEach { $0.buildMatches() } - refreshBrackets() + refreshGroupStages() } func resetStructure() { @@ -242,13 +324,12 @@ class Tournament : ModelObject, Storable { } - func refreshBrackets() { -// completeEntries.forEach { entrant in -// if entrant.bracketPosition > 0 { -// entrant.resetBracketPosition() -// } -// } -// + func refreshGroupStages() { + unsortedTeams().forEach { team in + team.groupStage = nil + team.groupStagePosition = nil + } + if groupStageCount > 0 { switch groupStageOrderingMode { case .random: @@ -262,28 +343,31 @@ class Tournament : ModelObject, Storable { } func setBrackets(randomize: Bool) { - let numberOfBracketsAsInt = groupStages().count + let groupStages = groupStages() + let numberOfBracketsAsInt = groupStages.count // let teamsPerBracket = Int(teamsPerBracket) if groupStageCount != numberOfBracketsAsInt { buildGroupStages() return } - let max = groupStages().map { $0.size }.reduce(0,+) -// var chunks = orderedEntries.filter { $0.wcFinalTable == false }.suffix(Int(max)).chunked(into: numberOfBracketsAsInt) -// for (index, _) in chunks.enumerated() { -// if randomize { -// chunks[index].shuffle() -// } else if index % 2 != 0 { -// chunks[index].reverse() -// } -// -// print("Equipes \(chunks[index].map { $0.initialRank })") -// for (jIndex, _) in chunks[index].enumerated() { -// print("Position \(index+1) Poule \(orderedBrackets[jIndex].index)") -// chunks[index][jIndex].bracketPosition = orderedBrackets[jIndex].index -// chunks[index][jIndex].bracketPositions = [Int(index + 1)] -// } -// } + let max = groupStages.map { $0.size }.reduce(0,+) + var chunks = teams().filter { $0.wildCardBracket == false }.suffix(max).chunked(into: numberOfBracketsAsInt) + for (index, _) in chunks.enumerated() { + if randomize { + chunks[index].shuffle() + } else if index % 2 != 0 { + chunks[index].reverse() + } + + print("Equipes \(chunks[index].map { $0.weight })") + for (jIndex, _) in chunks[index].enumerated() { + print("Position \(index+1) Poule \(groupStages[jIndex].index)") + chunks[index][jIndex].groupStage = groupStages[jIndex].id + chunks[index][jIndex].groupStagePosition = index + + try? DataStore.shared.teamRegistrations.addOrUpdate(instance: chunks[index][jIndex]) + } + } } @@ -293,11 +377,13 @@ class Tournament : ModelObject, Storable { func addTeam(_ players: Set) { let team = TeamRegistration(tournament: id, registrationDate: Date()) + team.tournamentCategory = tournamentCategory + team.setWeight(from: Array(players)) try? DataStore.shared.teamRegistrations.addOrUpdate(instance: team) players.forEach { player in player.teamRegistration = team.id } - try? DataStore.shared.playerRegistrations.append(contentOfs: players) + try? DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: players) } var teamSortingType: TeamSortingType { @@ -350,7 +436,12 @@ class Tournament : ModelObject, Storable { TournamentCategory(rawValue: federalCategory) ?? .men } set { - federalCategory = newValue.rawValue + if federalCategory != newValue.rawValue { + federalCategory = newValue.rawValue + updateWeights() + } else { + federalCategory = newValue.rawValue + } } } @@ -405,6 +496,12 @@ class Tournament : ModelObject, Storable { return matchFormat } } + + override func deleteDependencies() throws { + try Store.main.deleteDependencies(items: self.unsortedTeams()) + try Store.main.deleteDependencies(items: self.groupStages()) + //try Store.main.deleteDependencies(items: self.rounds()) + } } extension Tournament { diff --git a/PadelClub/Extensions/Sequence+Extensions.swift b/PadelClub/Extensions/Sequence+Extensions.swift index e97bea2..90808ff 100644 --- a/PadelClub/Extensions/Sequence+Extensions.swift +++ b/PadelClub/Extensions/Sequence+Extensions.swift @@ -20,3 +20,20 @@ extension Sequence { AnySequence(zip(self, self.dropFirst())) } } + +extension Sequence { + func concurrentForEach( + _ operation: @escaping (Element) async -> Void + ) async { + // A task group automatically waits for all of its + // sub-tasks to complete, while also performing those + // tasks in parallel: + await withTaskGroup(of: Void.self) { group in + for element in self { + group.addTask { + await operation(element) + } + } + } + } +} diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 9459e65..9efc40d 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -19,6 +19,10 @@ extension String { var canonicalVersion: String { trimmed.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ").folding(options: .diacriticInsensitive, locale: .current).lowercased() } + + var canonicalVersionWithPunctuation: String { + trimmed.folding(options: .diacriticInsensitive, locale: .current).lowercased() + } } extension String { diff --git a/PadelClub/Extensions/URL+Extensions.swift b/PadelClub/Extensions/URL+Extensions.swift index 2a13bde..6afb088 100644 --- a/PadelClub/Extensions/URL+Extensions.swift +++ b/PadelClub/Extensions/URL+Extensions.swift @@ -49,3 +49,42 @@ extension URL { Bundle.main.url(forResource: "SeedData", withExtension: nil) } } + +extension URL { + func getUnrankedValue() -> Int? { + // Read the contents of the file + guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { + return nil + } + + // Split the contents by newline characters + let lines = fileContents.components(separatedBy: .newlines) + + // Get the last non-empty line + var lastLine: String? + for line in lines.reversed() { + let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmedLine.isEmpty { + lastLine = trimmedLine + break + } + } + + guard let rankString = lastLine?.components(separatedBy: ";").dropFirst().first, let rank = Int(rankString) else { + return nil + } + // Define the regular expression pattern + let pattern = "\\b\(NSRegularExpression.escapedPattern(for: rankString))\\b" + + // Create the regular expression object + guard let regex = try? NSRegularExpression(pattern: pattern) else { + return nil + } + + // Get the matches + let matches = regex.matches(in: fileContents, range: NSRange(fileContents.startIndex..., in: fileContents)) + + // Return the count of matches + return matches.count + rank - 1 + } +} diff --git a/PadelClub/Info.plist b/PadelClub/Info.plist new file mode 100644 index 0000000..5f5eebc --- /dev/null +++ b/PadelClub/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDocumentTypes + + + CFBundleTypeRole + Editor + CFBundleTypeName + CSV,XLS + LSHandlerRank + Alternate + LSItemContentTypes + + public.content + public.data + + + + + diff --git a/PadelClub/Manager/FileImportManager.swift b/PadelClub/Manager/FileImportManager.swift index 4c06819..2ddd5fc 100644 --- a/PadelClub/Manager/FileImportManager.swift +++ b/PadelClub/Manager/FileImportManager.swift @@ -10,6 +10,248 @@ import Foundation class FileImportManager { static let shared = FileImportManager() + func foundInWomenData(license: String?) -> Bool { + guard let license = license?.strippedLicense else { + return false + } + do { + return try SourceFile.allFilesSortedByDate(false).first(where: { + let fileContent = try String(contentsOf: $0) + return fileContent.contains(";\(license);") + }) != nil + } catch { + print("history", error) + return false + } + } + + func foundInMenData(license: String?) -> Bool { + guard let license = license?.strippedLicense else { + return false + } + do { + return try SourceFile.allFilesSortedByDate(true).first(where: { + let fileContent = try String(contentsOf: $0) + return fileContent.contains(";\(license);") + }) != nil + } catch { + print("history", error) + return false + } + } + + enum FileProvider: CaseIterable, Identifiable { + var id: Self { self } + + case frenchFederation + case unknown + + var localizedLabel: String { + switch self { + case .frenchFederation: + return "FFT" + case .unknown: + return "Inconnu" + } + } + } + + struct TeamHolder: Identifiable { + let id: UUID = UUID() + let playerOne: PlayerRegistration + let playerTwo: PlayerRegistration + let weight: Int + let tournamentCategory: TournamentCategory + let previousTeam: TeamRegistration? + + init(playerOne: PlayerRegistration, playerTwo: PlayerRegistration, tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?) { + self.playerOne = playerOne + self.playerTwo = playerTwo + self.tournamentCategory = tournamentCategory + self.previousTeam = previousTeam + self.weight = playerOne.weight + playerTwo.weight + } + + func formattedSeed(in teams: [TeamHolder]) -> String { + if let index = teams.firstIndex(where: { $0.id == id }) { + return "#\(index + 1)" + } else { + return "###" + } + } + } + + static let FFT_ASSIMILATION_WOMAN_IN_MAN = "A calculer selon la pondération en vigueur" + + func createTeams(from fileContent: String, tournament: Tournament, fileProvider: FileProvider = .frenchFederation) async -> [TeamHolder] { + let lines = fileContent.components(separatedBy: "\n") + guard let firstLine = lines.first else { return [] } + var separator = "," + if firstLine.contains(";") { + separator = ";" + } + let headerCount = firstLine.components(separatedBy: separator).count + var results: [TeamHolder] = [] + if headerCount == 23 && fileProvider == .unknown { //PBL + let fetchRequest = ImportedPlayer.fetchRequest() + let federalContext = PersistenceController.shared.localContainer.viewContext + + lines.dropFirst().forEach { line in + let data = line.components(separatedBy: separator) + if data.count == 23 { + +// let team = Team(context: context) +// let brand = Brand(context: context) +// brand.title = data[2].trimmed +// brand.qualifier = data[0].trimmed +// brand.country = data[1].trimmed +// brand.lineOfBusiness = data[3].trimmed +// if brand.lineOfBusiness == "Bâtiment / Immo" { //quick fix +// brand.lineOfBusiness = "Bâtiment / Immo / Transport" +// } +// brand.name = data[4].trimmed +// team.brand = brand +// +// for i in 0...5 { +// let sex = data[i*3+5] +// let lastName = data[i*3+6].trimmed +// let firstName = data[i*3+7].trimmed +// if lastName.isEmpty == false { +// let playerOne = Player(context: context) +// let predicate = NSPredicate(format: "(canonicalLastName matches[cd] %@ OR canonicalLastName matches[cd] %@) AND (canonicalFirstName matches[cd] %@ OR canonicalFirstName matches[cd] %@)", lastName, lastName.removePunctuationAndHyphens, firstName, firstName.removePunctuationAndHyphens) +// fetchRequest.predicate = predicate +// if let playerFound = try? federalContext.fetch(fetchRequest).first { +// playerOne.updateWithImportedPlayer(playerFound) +// } else { +// playerOne.lastName = lastName +// playerOne.firstName = firstName +// playerOne.sex = sex == "H" ? 1 : sex == "F" ? 0 : -1 +// playerOne.currentRank = tournament?.lastRankMan ?? 0 +// } +// team.addToPlayers(playerOne) +// } +// } +// team.category = TournamentCategory.men.importingRawValue +// +// if let players = team.players, players.count > 0 { +// results.append(team) +// } else { +// context.delete(team) +// } + } + } + + + return results + } else if headerCount <= 18 && fileProvider == .frenchFederation { + Array(lines.dropFirst()).chunked(into: 2).forEach { teamLines in + if teamLines.count == 2 { + let dataOne = teamLines[0].replacingOccurrences(of: "\"", with: "").components(separatedBy: separator) + var dataTwo = teamLines[1].replacingOccurrences(of: "\"", with: "").components(separatedBy: separator) + if dataOne[11] != dataTwo[3] || dataOne[12] != dataTwo[4] { + if let found = lines.map({ $0.replacingOccurrences(of: "\"", with: "").components(separatedBy: separator) }).first(where: { components in + return dataOne[11] == components[3] && dataOne[12] == components[4] + }) { + dataTwo = found + } + } + if dataOne.count == dataTwo.count { + let category = dataOne[0] + + var tournamentCategory: TournamentCategory { + switch category { + case "Double Messieurs": + return .men + case "Double Dames": + return .women + case "Double Mixte": + return .mix + default: + return .men + } + } + + let resultOne = Array(dataOne.dropFirst(3).dropLast()) + let resultTwo = Array(dataTwo.dropFirst(3).dropLast()) + let sexUnknown: Bool = (resultOne.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) || (resultTwo.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) + + var sexPlayerOne : Int { + switch tournamentCategory { + case .men: return 1 + case .women: return 0 + case .mix: return 0 + } + } + + var sexPlayerTwo : Int { + switch tournamentCategory { + case .men: return 1 + case .women: return 0 + case .mix: return 1 + } + } + + let playerOne = PlayerRegistration(federalData: Array(resultOne[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown) + playerOne.setWeight(in: tournament) + let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown) + playerTwo.setWeight(in: tournament) + let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + results.append(team) + } + } + } + return results + } else if headerCount > 18 && fileProvider == .frenchFederation { + lines.dropFirst().forEach { line in + let data = line.components(separatedBy: separator) + if data.count > 18 { + let category = data[0] + let sexUnknown: Bool = (data.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) + var tournamentCategory: TournamentCategory { + switch category { + case "Double Messieurs": + return .men + case "Double Dames": + return .women + case "Double Mixte": + return .mix + default: + return .men + } + } + let result = Array(data.dropFirst(3).dropLast()) + + var sexPlayerOne : Int { + switch tournamentCategory { + case .men: return 1 + case .women: return 0 + case .mix: return 1 + } + } + + var sexPlayerTwo : Int { + switch tournamentCategory { + case .men: return 1 + case .women: return 0 + case .mix: return 0 + } + } + + let playerOne = PlayerRegistration(federalData: Array(result[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown) + playerOne.setWeight(in: tournament) + let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown) + playerTwo.setWeight(in: tournament) + + let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + results.append(team) + } + } + return results + } else { + return [] + } + } + func importDataFromFFT() async -> String? { if let importingDate = SourceFile.mostRecentDateAvailable { for source in SourceFile.allCases { diff --git a/PadelClub/Manager/Network/NetworkManager.swift b/PadelClub/Manager/Network/NetworkManager.swift index 2fdf8fb..dc2ef7b 100644 --- a/PadelClub/Manager/Network/NetworkManager.swift +++ b/PadelClub/Manager/Network/NetworkManager.swift @@ -35,10 +35,11 @@ class NetworkManager { 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) + print("dl rank data ok", lastDateString, fileName) } else if urlResponse.statusCode == 404 && fileName == "MESSIEURS" { + print("dl rank data failed", lastDateString, fileName) throw NetworkManagerError.fileNotYetAvailable } } diff --git a/PadelClub/Manager/SourceFileManager.swift b/PadelClub/Manager/SourceFileManager.swift index 428e226..07fc1f5 100644 --- a/PadelClub/Manager/SourceFileManager.swift +++ b/PadelClub/Manager/SourceFileManager.swift @@ -7,6 +7,97 @@ import Foundation +class SourceFileManager { + static let shared = SourceFileManager() + + 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()) + } + } + + 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) + } + } + + 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 SourceFile.mostRecentDateAvailable == nil { + if let previousDate = Calendar.current.date(byAdding: .month, value: -1, to: current) { + await fetchData(fromDate: previousDate) + } + } + } + + } + + func getAllFiles() async { + let dates = monthsBetweenDates(startDateString: "08-2022", endDateString: Date().monthYearFormatted) + .compactMap { + URL.importDateFormatter.date(from: $0) + } + .filter { date in + SourceFile.allFiles.contains(where: { $0.dateFromPath == date }) == false + } + + await dates.concurrentForEach { date in + await self.fetchData(fromDate: date) + } + } + + func monthsBetweenDates(startDateString: String, endDateString: String) -> [String] { + let dateFormatter = URL.importDateFormatter + + guard let startDate = dateFormatter.date(from: startDateString), + let endDate = dateFormatter.date(from: endDateString) else { + return [] + } + + var months: [String] = [] + var currentDate = startDate + let calendar = Calendar.current + + while currentDate <= endDate { + let monthString = dateFormatter.string(from: currentDate) + months.append(monthString) + + guard let nextMonthDate = calendar.date(byAdding: .month, value: 1, to: currentDate) else { + break + } + currentDate = nextMonthDate + } + return months + } +} + enum SourceFile: String, CaseIterable { case dames = "DAMES" case messieurs = "MESSIEURS" diff --git a/PadelClub/Manager/SwiftParser.swift b/PadelClub/Manager/SwiftParser.swift index 56823de..de0bda2 100644 --- a/PadelClub/Manager/SwiftParser.swift +++ b/PadelClub/Manager/SwiftParser.swift @@ -16,18 +16,18 @@ struct Line: Identifiable { let rawValue: String let data: [String?] - func updatePlayer(player: Player) { - player.clubName = data[11] - player.ligue = data[9] - player.country = data[4] - player.license = data[5] - player.inscriptionRank = rank - player.currentRank = rank - player.tournamentCount = Int16(tournamentCount) - player.points = points ?? 0 - player.codeClub = data[10] - player.assimilation = assimilation - } +// func updatePlayer(player: Player) { +// player.clubName = data[11] +// player.ligue = data[9] +// player.country = data[4] +// player.license = data[5] +// player.inscriptionRank = rank +// player.currentRank = rank +// player.tournamentCount = Int16(tournamentCount) +// player.points = points ?? 0 +// player.codeClub = data[10] +// player.assimilation = assimilation +// } var points: Double? { if let pointsValue { @@ -55,9 +55,9 @@ struct Line: Identifiable { data[1] ?? nil } - var rank: Int64 { + var rank: Int { if let rankValue { - return Int64(rankValue) ?? 0 + return Int(rankValue) ?? 0 } return 0 } @@ -83,7 +83,7 @@ struct CSVParser: AsyncSequence, AsyncIteratorProtocol { self.url = url self.seperator = seperator self.lineIterator = url.lines.makeAsyncIterator() - self.maleData = url.path().contains(PDFSource.messieurs.rawValue) + self.maleData = url.path().contains(SourceFile.messieurs.rawValue) } mutating func last() async throws -> Line? { diff --git a/PadelClub/Manager/Tips.swift b/PadelClub/Manager/Tips.swift index a9c1c2c..06bdc5b 100644 --- a/PadelClub/Manager/Tips.swift +++ b/PadelClub/Manager/Tips.swift @@ -190,6 +190,10 @@ struct InscriptionManagerFileInputTip: Tip { } struct InscriptionManagerWomanRankTip: Tip { + var image: Image? { + Image(systemName: "figure.dress.line.vertical.figure") + } + var title: Text { Text("Rang d'une joueuse dans un tournoi messieurs") } diff --git a/PadelClub/Views/Components/StepperView.swift b/PadelClub/Views/Components/StepperView.swift index 2e15a57..6505393 100644 --- a/PadelClub/Views/Components/StepperView.swift +++ b/PadelClub/Views/Components/StepperView.swift @@ -66,7 +66,7 @@ struct StepperView: View { } fileprivate func _plusIsDisabled() -> Bool { - count >= (maximum ?? Int.max) + count >= (maximum ?? 100_000) } fileprivate func _add() { diff --git a/PadelClub/Views/ContentView.swift b/PadelClub/Views/ContentView.swift index 050d62f..5d92628 100644 --- a/PadelClub/Views/ContentView.swift +++ b/PadelClub/Views/ContentView.swift @@ -60,19 +60,19 @@ struct ContentView: View { // let club: Club = Club(name: "test\(id)", address: "some address") // self.dataStore.clubs.addOrUpdate(instance: club) - for _ in 0...20 { - var clubs: [Club] = [] - for _ in 0...20 { - let id = (0...1000000).randomElement()! - let club: Club = Club(name: "test\(id)", acronym: "test", address: "some address") - clubs.append(club) - } - do { - try self.dataStore.clubs.append(contentOfs: clubs) - } catch { - Logger.error(error) - } - } +// for _ in 0...20 { +// var clubs: [Club] = [] +// for _ in 0...20 { +// let id = (0...1000000).randomElement()! +// let club: Club = Club(name: "test\(id)", acronym: "test", address: "some address") +// clubs.append(club) +// } +// do { +// try self.dataStore.clubs.append(contentOfs: clubs) +// } catch { +// Logger.error(error) +// } +// } } } diff --git a/PadelClub/Views/Event/EventCreationView.swift b/PadelClub/Views/Event/EventCreationView.swift index 9651a16..50478fb 100644 --- a/PadelClub/Views/Event/EventCreationView.swift +++ b/PadelClub/Views/Event/EventCreationView.swift @@ -80,7 +80,7 @@ struct EventCreationView: View { } try? dataStore.events.addOrUpdate(instance: event) } - try? dataStore.tournaments.append(contentOfs: tournaments) + try? dataStore.tournaments.addOrUpdate(contentOfs: tournaments) dismiss() } .clipShape(Capsule()) @@ -115,12 +115,6 @@ struct EventCreationView: View { RowButtonView(title: "Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") { let tournament = Tournament.newEmptyInstance() - -// let tournament = Tournament(context: viewContext) -// tournament.tournamentLevel = TournamentLevel.mostUsed(tournaments: tournaments) -// tournament.tournamentCategory = TournamentCategory.mostUsed(tournaments: tournaments).next -// tournament.federalTournamentAge = FederalTournamentAge.mostUsed(tournaments: tournaments) -// self.tournaments.append(tournament) } } diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 8cc69c5..23b05ef 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -23,9 +23,10 @@ struct GroupStageView: View { var groupStageView: some View { ForEach(0..<(groupStage.size), id: \.self) { index in // let entrant : Entrant? = runningGroupStageOrderedByScore ? groupStage.orderedByScore[Int(index)] : groupStage.entrantAtIndex(Int(index)) -// if let entrant { + if let team = groupStage.teamsAt(index) { + Text(team.teamLabel()) // GroupStageEntrantMenuView(entrant: entrant, groupStage: groupStage, index: index.intValue) -// } else { + } else { Menu { // Section { // EntrantPickerView(groupStage: groupStage, index: Int(index)) @@ -48,7 +49,7 @@ struct GroupStageView: View { Text("Aucune équipe") } } -// } + } } } diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 35688bb..b194d33 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -56,6 +56,7 @@ struct MainView: View { .environmentObject(dataStore) .task { await self._checkSourceFileAvailability() + await self._downloadPreviousDate() } .refreshable { Task { @@ -113,7 +114,7 @@ struct MainView: View { print("check files on internet") print("check if any files on internet are more recent than here") checkingFiles = true - await fetchData() + await SourceFileManager.shared.fetchData() checkingFilesAttempt += 1 checkingFiles = false @@ -130,6 +131,8 @@ struct MainView: View { await _calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) } importingFiles = false + + _downloadPreviousDate() } } @@ -138,55 +141,11 @@ struct MainView: View { lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: 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) - } - } + private func _downloadPreviousDate() { + Task { + await SourceFileManager.shared.getAllFiles() } - } - } fileprivate extension View { diff --git a/PadelClub/Views/Team/TeamDetailView.swift b/PadelClub/Views/Team/TeamDetailView.swift index 757a898..fce9049 100644 --- a/PadelClub/Views/Team/TeamDetailView.swift +++ b/PadelClub/Views/Team/TeamDetailView.swift @@ -12,7 +12,7 @@ struct TeamDetailView: View { var team: TeamRegistration var body: some View { - if team.players().isEmpty { + if team.unsortedPlayers().isEmpty { Text("Aucun joueur, espace réservé") } else { ForEach(team.players()) { player in diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 0276da9..ecdb624 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -8,11 +8,292 @@ import SwiftUI struct FileImportView: View { + @Environment(Tournament.self) var tournament: Tournament + @Environment(\.dismiss) private var dismiss + + let fileContent: String? + + @State private var teams: [FileImportManager.TeamHolder] = [] + @State private var isShowing = false + @State private var didImport = false + @State private var convertingFile = false + @State private var errorMessage: String? = nil + @State private var forceRankUpdate: Bool = false + + @State private var selectedOptions: Set = Set() + + @State private var fileProvider: FileImportManager.FileProvider = .frenchFederation + let federalLink = URL(string: "https://beach-padel.app.fft.fr/beachja/index/")! + + private var filteredTeams: [FileImportManager.TeamHolder] { + return teams.filter { $0.tournamentCategory == tournament.tournamentCategory }.sorted(by: \.weight) + } + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + List { + if teams.isEmpty { + Section { + + Link(destination: federalLink) { + Label("Ouvrir beach-padel.app.fft.fr", systemImage: "tennisball") + } + + Button { + convertingFile = false + isShowing.toggle() + } label: { + Label("Choisir le fichier", systemImage: "square.and.arrow.down") + } + } header: { + + } footer: { + VStack(alignment: .leading) { + Text("Fichier provenant de beach-padel.app.fft.fr") + Text("Format XLS ou CSV, onglet inscriptions ou joueurs") + } + } + } + + if filteredTeams.isEmpty == false { + Section { + ForEach(TeamImportStrategy.allCases, id: \.self) { strategy in + LabeledContent { + Toggle(isOn: .init(get: { + selectedOptions.contains(strategy) + }, set: { selected in + if selected { + selectedOptions.insert(strategy) + } else { + selectedOptions.remove(strategy) + + } + })) {} + } label: { + Text(strategy.titleLabel()) + Text(strategy.descriptionLabel()) + } + + } + } header: { + Text("Stratégie d'importation") + } + } + + if convertingFile { + Section { + LabeledContent { + ProgressView() + } label: { + Text("Importation en cours") + } + } + } + + if let errorMessage { + Section { + Text(errorMessage) + } header: { + Text("Erreur") + } + } + +// if tournament.entriesCount > 0 { +// Section { +// if tournament.inscriptionClosed { +// Label("Les inscriptions clôturées", systemImage: "lock") +// Text("Si le poids des équipes a changé, aucun déplacement entre les poules et le tableau n'est possible. Par contre, le classement sera mis à jour au sein des poules et du tableau, respectivement, en fonction de leur nouveau poids.") +// Toggle(isOn: $forceRankUpdate) { +// Text("Ne pas en tenir compte") +// } +// } else { +// Label("Les inscriptions sont ouvertes", systemImage: "lock.open") +// Text("Si le poids des équipes a changé, le classement de toutes les équipes sera mis à jour en fonction de leur nouveau poids.") +// } +// } +// } + + if filteredTeams.isEmpty && teams.isEmpty == false { + @Bindable var tournament = tournament + Section { + Text("Aucune équipe \(tournament.tournamentCategory.importingRawValue) détectée mais \(teams.count) équipes sont dans le fichier") + Picker(selection: $tournament.tournamentCategory) { + ForEach(TournamentCategory.allCases) { category in + Text(category.importingRawValue).tag(category) + } + } label: { + Text("Modifier la catégorie du tournoi ?") + } + .onChange(of: tournament.tournamentCategory) { + save() + } + } + } else if teams.isEmpty && didImport == true { + Section { + ContentUnavailableView("Aucune équipe détectée", systemImage: "person.2.slash") + } + } else if didImport { + Section { + let previousTeams = tournament.teams() + ForEach(filteredTeams) { team in + LabeledContent { + HStack { + if let previousTeam = team.previousTeam { + Text(previousTeam.formattedSeed(in: previousTeams)) + } + Text("->") + Text(team.formattedSeed(in: filteredTeams)) + } + } label: { + VStack(alignment: .leading) { + Text(team.playerOne.playerLabel()) + Text(team.playerTwo.playerLabel()) + } + } + } + } header: { + HStack { + Text("Équipes \(tournament.tournamentCategory.importingRawValue) détectées dans ce fichier") + Spacer() + Text(filteredTeams.count.formatted()) + } + } + } + } + .onAppear { + if let fileContent { + Task { + await _startImport(fileContent: fileContent) + } + } + } + .fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText], allowsMultipleSelection: false, onCompletion: { results in + + switch results { + case .success(let fileurls): + if let selectedFile = fileurls.first { + if selectedFile.startAccessingSecurityScopedResource() { + convertingFile = true + teams.removeAll() + Task { + do { + var fileContent: String? + if selectedFile.lastPathComponent.hasSuffix("xls") { + fileContent = try await CloudConvert.manager.uploadFile(selectedFile) + } else { + fileContent = try String(contentsOf: selectedFile) + } + + if let fileContent { + await _startImport(fileContent: fileContent) + } + selectedFile.stopAccessingSecurityScopedResource() + } catch { + errorMessage = error.localizedDescription + } + } + } else { + // Handle denied access + } + } + case .failure(let error): + errorMessage = error.localizedDescription + } + }) + + .onOpenURL { url in + do { + let fileContent = try String(contentsOf: url) + Task { + await _startImport(fileContent: fileContent) + } + } catch { + errorMessage = error.localizedDescription + } + } + .navigationTitle("Import") + .navigationBarTitleDisplayMode(.large) + .onDisappear { + //viewContext.rollback() + } + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Annuler", role: .cancel) { + dismiss() + } + } + + ToolbarItem(placement: .bottomBar) { + Button { +// tournament.updateTournamentEntriesWith(teams: filteredTeams, viewContext: viewContext) +// save() + tournament.importTeams(filteredTeams) + + + dismiss() + } label: { + Text("Valider") + } + .buttonStyle(.borderedProminent) + .disabled(teams.isEmpty) + } + } + } + + func _startImport(fileContent: String) async { + await MainActor.run { + convertingFile = true + teams.removeAll() + } + self.teams = await FileImportManager.shared.createTeams(from: fileContent, tournament: tournament, fileProvider: fileProvider) + await MainActor.run { + convertingFile = false + didImport = true + } + } + + func save() { + } } #Preview { - FileImportView() + FileImportView(fileContent: nil) + .environment(Tournament.mock()) +} + +enum TeamImportStrategy: CaseIterable { + case keepPreviousData + case notFoundAreWalkOut + case deleteBeforeImport + case updatePosition + case updatePositionWithinBlock + + func titleLabel() -> String { + switch self { + case .keepPreviousData: + "Gardez les données existantes" + case .notFoundAreWalkOut: + "Mettre les équipes manquantes WO" + case .deleteBeforeImport: + "Effacer avant d'importer" + case .updatePosition: + "Modifier les positions" + case .updatePositionWithinBlock: + "Modidier les positions par bloc" + } + } + func descriptionLabel() -> String { + switch self { + case .keepPreviousData: + "Si l'équipe déjà présente, garde la date d'inscription" + case .notFoundAreWalkOut: + "Si une équipe déjà présente n'est pas dans la nouvelle liste, elle sera mise à WO" + case .deleteBeforeImport: + "Écrase les données précédentes avant d'importer" + case .updatePosition: + "Mets à jour les positions si changement de poids d'équipe" + case .updatePositionWithinBlock: + "Mets à jour les positions seulement au sein du tableau et des poules séparement" + } + } } diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionTipsView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionTipsView.swift index cfdca41..669a5b7 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionTipsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionTipsView.swift @@ -55,9 +55,7 @@ struct InscriptionTipsView: View { } Section { - ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description: - Text("Vous n'avez encore aucune équipe dans votre liste d'attente.") - ) + ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description: Text("Vous n'avez encore aucune équipe dans votre liste d'attente.")) } // if let mostRecentDate, let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.isOver == false { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift index 399af95..0afe572 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift @@ -13,7 +13,7 @@ struct TournamentFieldsManagerView: View { var body: some View { @Bindable var tournament = tournament - Stepper(value: $tournament.courtCount, in: 1...Int.max) { + Stepper(value: $tournament.courtCount, in: 1...100_000) { LabeledContent { Text(tournament.courtCount.formatted()) } label: { diff --git a/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift b/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift index 7b9ca3d..5f200c1 100644 --- a/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift @@ -8,11 +8,87 @@ import SwiftUI struct UpdateSourceRankDateView: View { + @EnvironmentObject var dataStore: DataStore + @Binding var currentRankSourceDate: Date? + @Binding var confirmUpdateRank: Bool + @State private var updatingRank = false + var tournament: Tournament + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + NavigationStack { + List { + let suffix = (false ? "Les incriptions sont closes. Les équipes ne pourront pas être déplacé entre les poules et le tableau. Seule leur position au sein des poules et du tableau, respectivement, seront modifiée." : "Les inscriptions sont toujours ouvertes. Les équipes pourront être déplacé entre les poules et le tableau.") + + Section { + Text("Vous êtes sur le point de mettre à jour les rangs des équipes, cela affectera leur position." + "\n" + suffix) + } + + RowButtonView(title: "Valider") { + updatingRank = true + //buildMoveArray() + Task { + do { + try await tournament.updateRank(to: currentRankSourceDate) + await MainActor.run { + if tournament.state() == .build { + //manageEntriesMovement() + } else { + //save() + tournament.unsortedPlayers().forEach { player in + player.setWeight(in: tournament) + } + + try? dataStore.playerRegistrations.addOrUpdate(contentOfs: tournament.unsortedPlayers()) + + tournament.unsortedTeams().forEach { team in + team.setWeight(from: team.players()) + } + + try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) + + try? dataStore.tournaments.addOrUpdate(instance: tournament) + } + + updatingRank = false + confirmUpdateRank = false + } + } catch { + + } + } + }.disabled(updatingRank) + + if updatingRank { + Section { + let message = currentRankSourceDate == nil ? "Mise à jour des classements" : "Mise à jour des classements provenant de \(currentRankSourceDate!.monthYearFormatted) pour tous les joueurs." + ContentUnavailableView("Mise à jour", systemImage: "exclamationmark.triangle", description: Text(message)) + } + + Section { + HStack { + Spacer() + ProgressView() + Spacer() + } + } + } + } + .navigationTitle("Attention") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(role: .cancel) { + confirmUpdateRank = false + } label: { + Text("Annuler") + } + } + } + } + .interactiveDismissDisabled(updatingRank) } } #Preview { - UpdateSourceRankDateView() + UpdateSourceRankDateView(currentRankSourceDate: .constant(Date()), confirmUpdateRank: .constant(true), tournament: Tournament.mock()) } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 6ff0801..7c04217 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -18,16 +18,36 @@ struct InscriptionManagerView: View { var tournament: Tournament @State private var searchField: String = "" + @State private var presentSearch: Bool = false @State private var presentPlayerSearch: Bool = false @State private var presentPlayerCreation: Bool = false + @State private var presentImportView: Bool = false @State private var createdPlayers: Set = Set() @State private var testCreatedPlayers: Set = Set() @State private var editedTeam: TeamRegistration? @State private var pasteString: String? - + @State private var currentRankSourceDate: Date? + @State private var confirmUpdateRank = false + @State private var updatingRank = false + let slideToDeleteTip = SlideToDeleteTip() let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip() - + let categoryOption: PlayerFilterOption + let filterable: Bool + + init(tournament: Tournament) { + self.tournament = tournament + _currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate) + switch tournament.tournamentCategory { + case .women: + categoryOption = .female + filterable = false + default: + categoryOption = .all + filterable = true + } + } + private func _pastePredicate(pasteField: String, mostRecentDate: Date?) -> NSPredicate? { let text = pasteField.canonicalVersion @@ -73,6 +93,7 @@ struct InscriptionManagerView: View { fetchPlayers.first(where: { id == $0.license }) }.forEach { player in let player = PlayerRegistration(importedPlayer: player) + player.setWeight(in: tournament) currentSelection.insert(player) } @@ -116,8 +137,14 @@ struct InscriptionManagerView: View { } if editedTeam == nil { - RowButtonView(title: "Ajouter l'équipe") { - _createTeam() + if testCreatedPlayers.isEmpty { + RowButtonView(title: "Bloquer une place") { + _createTeam() + } + } else { + RowButtonView(title: "Ajouter l'équipe") { + _createTeam() + } } } else { RowButtonView(title: "Modifier l'équipe") { @@ -126,17 +153,7 @@ struct InscriptionManagerView: View { } if let pasteString { - -// if pasteString.licencesFound().count == 2 { -// let hits = fetchPlayers.filter { $0.hitForSearch(pasteString) == 100 } -// if hits.count == 2 { -// ForEach(hits) { hit in -// -// createdPlayers -// } -// } -// } -// + Section { Text(pasteString) } footer: { @@ -152,12 +169,29 @@ struct InscriptionManagerView: View { } } - Section { - ForEach(fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })) { player in - ImportedPlayerView(player: player).tag(player.license!) + if fetchPlayers.isEmpty { + ContentUnavailableView { + Label("Aucun résultat", systemImage: "person.2.slash") + } description: { + Text("Aucun joueur classé n'a été trouvé dans ce message.") + } actions: { + RowButtonView(title: "Créer un joueur non classé") { + presentPlayerCreation = true + } + + RowButtonView(title: "Effacer cette recherche") { + self.pasteString = nil + } + } + + } else { + Section { + ForEach(fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })) { player in + ImportedPlayerView(player: player).tag(player.license!) + } + } header: { + Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix) } - } header: { - Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix) } } } @@ -188,28 +222,76 @@ struct InscriptionManagerView: View { private func _teamRegisteredView() -> some View { List { Section { - if tournament.tournamentCategory == .men && tournament.femalePlayers().isEmpty == false { - - TipView(inscriptionManagerWomanRankTip) - .tipStyle(tint: nil) + rankingDateSourcePickerView(showDateInLabel: true) + + let duplicates = tournament.duplicates() + DisclosureGroup { + if duplicates.isEmpty == false { + ForEach(duplicates) { player in + PlayerView(player: player) + } + } + } label: { + LabeledContent { + Text(duplicates.count.formatted()) + } label: { + Text("Doublons") + } } } header: { Text("Informations") } + if tournament.tournamentCategory == .men && tournament.femalePlayers().isEmpty == false { + Section { + TipView(inscriptionManagerWomanRankTip) + .tipStyle(tint: nil) + } + } + Section { TipView(slideToDeleteTip) .tipStyle(tint: nil) } - ForEach(tournament.teams()) { team in + let unfilteredTeams = tournament.teams() + let teams = searchField.isEmpty ? unfilteredTeams : unfilteredTeams.filter({ $0.contains(searchField.canonicalVersion) }) + + if teams.isEmpty && searchField.isEmpty == false { + ContentUnavailableView { + Label("Aucun résultat", systemImage: "person.2.slash") + } description: { + Text("\(searchField) est introuvable dans les équipes inscrites.") + } actions: { + RowButtonView(title: "Modifier la recherche") { + searchField = "" + presentSearch = true + } + + RowButtonView(title: "Créer une équipe") { + Task { + await MainActor.run() { +// fetchPlayers.nsPredicate = _pastePredicate(pasteField: searchField, mostRecentDate: tournament.rankSourceDate) + pasteString = searchField + } + } + } + + RowButtonView(title: "D'accord") { + searchField = "" + presentSearch = false + } + } + } + + ForEach(teams) { team in Section { TeamRowView(team: team) } header: { HStack { - Text("Équipe") + Text("Équipe " + team.formattedSeed(in: unfilteredTeams)) Spacer() - Text(team.computedRank().formatted()) + Text(team.weight.formatted()) } } footer: { HStack { @@ -217,7 +299,7 @@ struct InscriptionManagerView: View { Menu { Button("Éditer") { editedTeam = team - team.players().forEach { player in + team.unsortedPlayers().forEach { player in createdPlayers.insert(player) testCreatedPlayers.insert(player.id) } @@ -229,14 +311,14 @@ struct InscriptionManagerView: View { LabelDelete() } } label: { - LabelOptions() + LabelOptions().labelStyle(.titleOnly) } } } .headerProminence(.increased) } } - .searchable(text: $searchField) + .searchable(text: $searchField, isPresented: $presentSearch, prompt: Text("Chercher parmi les équipes inscrites")) } var body: some View { @@ -244,7 +326,7 @@ struct InscriptionManagerView: View { _managementView() if testCreatedPlayers.isEmpty == false || pasteString != nil || editedTeam != nil { _buildingTeamView() - } else if tournament.teams().isEmpty { + } else if tournament.unsortedTeams().isEmpty { InscriptionTipsView() } else { _teamRegisteredView() @@ -255,6 +337,7 @@ struct InscriptionManagerView: View { SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption()) { players in players.forEach { player in let newPlayer = PlayerRegistration(importedPlayer: player) + newPlayer.setWeight(in: tournament) createdPlayers.insert(newPlayer) testCreatedPlayers.insert(newPlayer.id) } @@ -267,6 +350,23 @@ struct InscriptionManagerView: View { testCreatedPlayers.insert(p.id) } } + .sheet(isPresented: $presentImportView) { + NavigationStack { + FileImportView(fileContent: nil) + } + } + .onChange(of: currentRankSourceDate) { +// if let currentRankSourceDate, tournament.currentRankSourceDate != currentRankSourceDate { +// confirmUpdateRank = true +// } + confirmUpdateRank = true + } + .sheet(isPresented: $confirmUpdateRank, onDismiss: { + currentRankSourceDate = tournament.rankSourceDate + }) { + UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament) + } + .toolbar { if testCreatedPlayers.isEmpty == false { ToolbarItem(placement: .cancellationAction) { @@ -276,10 +376,22 @@ struct InscriptionManagerView: View { } } } + + ToolbarItem(placement: .topBarTrailing) { + Menu { + Button { + presentImportView = true + } label: { + Label("Importer", systemImage: "square.and.arrow.down") + } + } label: { + LabelOptions() + } + } } .navigationBarBackButtonHidden(testCreatedPlayers.isEmpty == false) .toolbarBackground(.visible, for: .navigationBar) - .navigationTitle("Inscription") + .navigationTitle("Inscriptions") .navigationBarTitleDisplayMode(.inline) } @@ -303,7 +415,7 @@ struct InscriptionManagerView: View { guard let first = strings.first else { return } Task { await MainActor.run() { - fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: nil) +// fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: tournament.rankSourceDate) pasteString = first } } @@ -329,6 +441,35 @@ struct InscriptionManagerView: View { .padding(16) } + + @ViewBuilder + func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View { + Section { + Picker(selection: $currentRankSourceDate) { + if currentRankSourceDate == nil { + Text("inconnu").tag(nil as Date?) + } + + let dates = Array(Set(SourceFile.allFilesSortedByDate(tournament.tournamentCategory.rankingDataSourceMale).map({ $0.dateFromPath }))).sorted().reversed() + + ForEach(dates, id: \.self) { date in + Text(date.monthYearFormatted).tag(date as Date?) + } + } label: { + Text("Classement mensuel utilisé") + if showDateInLabel { + if let currentRankSourceDate { + Text(currentRankSourceDate.monthYearFormatted) + } else { + Text("Choisir le mois") + } + } + } + .pickerStyle(.menu) + } + + } + private func _addPlayerSex() -> Int { switch tournament.tournamentCategory { case .men: diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 3ad50b2..e7a9681 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -35,7 +35,7 @@ struct TournamentView: View { NavigationLink(value: Screen.inscription) { LabeledContent { - Text(tournament.teams().count.formatted()) + Text(tournament.unsortedTeams().count.formatted()) } label: { Text("Inscriptions") }