From c31062fc70da1bb5efdc95a6ecca6b0d287a145a Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sun, 24 Mar 2024 17:23:58 +0100 Subject: [PATCH] inscription list management --- PadelClub.xcodeproj/project.pbxproj | 64 +++ .../Model.xcdatamodel/contents | 4 +- PadelClub/Data/Federal/FederalPlayer.swift | 125 ++++- PadelClub/Data/GroupStage.swift | 4 + PadelClub/Data/Match.swift | 93 ++- PadelClub/Data/MockData.swift | 12 + PadelClub/Data/PlayerRegistration.swift | 193 ++++++- PadelClub/Data/TeamRegistration.swift | 83 ++- PadelClub/Data/Tournament.swift | 80 ++- .../Extensions/Sequence+Extensions.swift | 6 + PadelClub/Extensions/String+Extensions.swift | 80 +++ PadelClub/Manager/PadelRule.swift | 19 +- PadelClub/ViewModel/SearchViewModel.swift | 6 +- PadelClub/Views/Club/ClubsView.swift | 2 +- PadelClub/Views/Components/Labels.swift | 6 + .../Views/GroupStage/GroupStagesView.swift | 19 +- PadelClub/Views/Match/MatchDateView.swift | 110 ++++ PadelClub/Views/Match/MatchDetailView.swift | 528 +++++++++++++++++- PadelClub/Views/Match/MatchRowView.swift | 12 +- PadelClub/Views/Match/MatchSetupView.swift | 35 ++ PadelClub/Views/Match/MatchSummaryView.swift | 247 +++++++- PadelClub/Views/Match/PlayerBlockView.swift | 87 +++ PadelClub/Views/Navigation/MainView.swift | 11 + .../Player/Components/PlayerPopoverView.swift | 229 ++++++++ .../Components/PlayerSexPickerView.swift | 48 ++ PadelClub/Views/Player/PlayerView.swift | 28 + .../Views/Shared/ImportedPlayerView.swift | 47 +- .../Shared/SelectablePlayerListView.swift | 24 + PadelClub/Views/Team/TeamDetailView.swift | 27 + PadelClub/Views/Team/TeamPickerView.swift | 22 + PadelClub/Views/Team/TeamRowView.swift | 21 + .../Components/InscriptionTipsView.swift | 88 +++ .../Screen/InscriptionManagerView.swift | 350 +++++++++++- .../Views/Tournament/TournamentView.swift | 31 +- 34 files changed, 2642 insertions(+), 99 deletions(-) create mode 100644 PadelClub/Views/Match/MatchDateView.swift create mode 100644 PadelClub/Views/Match/MatchSetupView.swift create mode 100644 PadelClub/Views/Match/PlayerBlockView.swift create mode 100644 PadelClub/Views/Player/Components/PlayerPopoverView.swift create mode 100644 PadelClub/Views/Player/Components/PlayerSexPickerView.swift create mode 100644 PadelClub/Views/Player/PlayerView.swift create mode 100644 PadelClub/Views/Team/TeamDetailView.swift create mode 100644 PadelClub/Views/Team/TeamPickerView.swift create mode 100644 PadelClub/Views/Team/TeamRowView.swift create mode 100644 PadelClub/Views/Tournament/Screen/Components/InscriptionTipsView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b0d8ad0..086901c 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -32,6 +32,11 @@ C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAC2B85FCCD00ADC637 /* User.swift */; }; C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB02B86375E00ADC637 /* MainUserView.swift */; }; C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB22B86387500ADC637 /* AccountView.swift */; }; + FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */; }; + FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */; }; + 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 */; }; 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 */; }; @@ -94,6 +99,11 @@ FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */; }; FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */; }; FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CFF2BAEEF6400A9A3BD /* MatchRowView.swift */; }; + FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D052BAF3C4200A9A3BD /* MatchSetupView.swift */; }; + FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D082BAF3D4000A9A3BD /* TeamDetailView.swift */; }; + 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 */; }; 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 */; }; @@ -186,6 +196,11 @@ C4A47DAC2B85FCCD00ADC637 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; C4A47DB02B86375E00ADC637 /* MainUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUserView.swift; sourceTree = ""; }; C4A47DB22B86387500ADC637 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = ""; }; + FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSexPickerView.swift; sourceTree = ""; }; + FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRowView.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; @@ -246,6 +261,11 @@ FF967CFF2BAEEF6400A9A3BD /* MatchRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchRowView.swift; sourceTree = ""; }; FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchSummaryView.swift; sourceTree = ""; }; FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchDetailView.swift; sourceTree = ""; }; + FF967D052BAF3C4200A9A3BD /* MatchSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchSetupView.swift; sourceTree = ""; }; + FF967D082BAF3D4000A9A3BD /* TeamDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamDetailView.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; @@ -400,6 +420,8 @@ FF1DC54D2BAB34FA00FD8220 /* Club */, FF967CF92BAEE11500A9A3BD /* GroupStage */, FF967CFE2BAEEF5A00A9A3BD /* Match */, + FF967D072BAF3D3000A9A3BD /* Team */, + FF089EB92BB011EE00F0AEC7 /* Player */, FF3F74F72B919F96004CFE0E /* Tournament */, C4A47D882B7BBB5000ADC637 /* Subscription */, C4A47D852B7BA33F00ADC637 /* User */, @@ -454,6 +476,24 @@ path = Components; sourceTree = ""; }; + FF089EB02BB001EA00F0AEC7 /* Components */ = { + isa = PBXGroup; + children = ( + FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */, + FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */, + ); + path = Components; + sourceTree = ""; + }; + FF089EB92BB011EE00F0AEC7 /* Player */ = { + isa = PBXGroup; + children = ( + FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */, + FF089EB02BB001EA00F0AEC7 /* Components */, + ); + path = Player; + sourceTree = ""; + }; FF1DC54D2BAB34FA00FD8220 /* Club */ = { isa = PBXGroup; children = ( @@ -604,6 +644,7 @@ FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */, FF8F26482BAE0B4100650388 /* TournamentFormatSelectionView.swift */, FF8F26492BAE0B4100650388 /* TournamentLevelPickerView.swift */, + FF089EB72BB00ABF00F0AEC7 /* InscriptionTipsView.swift */, ); path = Components; sourceTree = ""; @@ -621,12 +662,25 @@ isa = PBXGroup; children = ( FF967CFF2BAEEF6400A9A3BD /* MatchRowView.swift */, + FF967D052BAF3C4200A9A3BD /* MatchSetupView.swift */, FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */, FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */, + FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */, + FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */, ); path = Match; sourceTree = ""; }; + FF967D072BAF3D3000A9A3BD /* Team */ = { + isa = PBXGroup; + children = ( + FF967D082BAF3D4000A9A3BD /* TeamDetailView.swift */, + FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */, + FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */, + ); + path = Team; + sourceTree = ""; + }; FFD783FB2B91B919000F62A6 /* Agenda */ = { isa = PBXGroup; children = ( @@ -836,6 +890,7 @@ C4A47D8A2B7BBB6500ADC637 /* SubscriptionView.swift in Sources */, FF1DC5572BAB3AED00FD8220 /* ClubsView.swift in Sources */, FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */, + FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */, FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */, C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */, FF7091682B90F79F00AB08DA /* TournamentCellView.swift in Sources */, @@ -851,6 +906,7 @@ FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */, FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, + FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */, FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */, FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */, FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */, @@ -887,9 +943,11 @@ FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, + FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, + FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, @@ -905,21 +963,27 @@ FFF8ACD22B9238C3008466FA /* FileImportManager.swift in Sources */, FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */, FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */, + FF089EB82BB00ABF00F0AEC7 /* InscriptionTipsView.swift in Sources */, FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */, + FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */, FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */, FF967CFD2BAEE5F500A9A3BD /* GroupStageView.swift in Sources */, FF1DC5592BAB767000FD8220 /* Tips.swift in Sources */, FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */, + FF967D0D2BAF3EB300A9A3BD /* MatchDateView.swift in Sources */, FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */, FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */, + FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */, FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */, + FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */, C4A47D922B7BBBEC00ADC637 /* StoreItem.swift in Sources */, FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */, FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */, C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */, FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, + FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */, FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */, C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */, diff --git a/PadelClub/Data/Coredata/PadelClubApp.xcdatamodeld/Model.xcdatamodel/contents b/PadelClub/Data/Coredata/PadelClubApp.xcdatamodeld/Model.xcdatamodel/contents index 7d63e0b..3969c69 100644 --- a/PadelClub/Data/Coredata/PadelClubApp.xcdatamodeld/Model.xcdatamodel/contents +++ b/PadelClub/Data/Coredata/PadelClubApp.xcdatamodeld/Model.xcdatamodel/contents @@ -1,9 +1,9 @@ - + - + diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift index 7d69cbe..7bbed12 100644 --- a/PadelClub/Data/Federal/FederalPlayer.swift +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -7,12 +7,106 @@ import Foundation -extension ImportedPlayer { +protocol PlayerHolder { + + func getFirstName() -> String + func getLastName() -> String + func formattedRank() -> String + func formattedLicense() -> String + func getPoints() -> Double? + func getRank() -> Int? + func isUnranked() -> Bool + var male: Bool { get } + var tournamentPlayed: Int? { get } + var clubName: String? { get } + var ligueName: String? { get } + var assimilation: String? { get } +} + +extension PlayerHolder { var isAssimilated: Bool { assimilation == "Oui" } } + +extension ImportedPlayer: PlayerHolder { + + var tournamentPlayed: Int? { + Int(tournamentCount) + } + + func getPoints() -> Double? { + self.points + } + + func getFirstName() -> String { + self.firstName ?? "prénom inconnu" + } + + func getLastName() -> String { + self.lastName ?? "nom inconnu" + } + + func formattedLicense() -> String { + if let license { return license.computedLicense } + return "aucune licence" + } + + func getRank() -> Int? { + Int(rank) + } + + func isUnranked() -> Bool { + false + } + + func formattedRank() -> String { + rank.formatted() + } + + func isMalePlayer() -> Bool { + male + } + + func hitForSearch(_ searchText: String) -> Int { + var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current) + trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ") + trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .symbols, replacementString: " ") + + if trimmedSearchText.isEmpty { return 0 } + let tokens = trimmedSearchText.components(separatedBy: .whitespacesAndNewlines).filter { $0.isEmpty == false } + if let license, trimmedSearchText.contains(license) { + return 100 + } + + let label = canonicalFullName! + if tokens.count > 1 { + var wordFound = 0 + if trimmedSearchText.lowercased().components(separatedBy: .whitespacesAndNewlines).count > 1 { + let searchFields: Set = Set([firstName!.canonicalVersion.components(separatedBy: .whitespacesAndNewlines), lastName!.canonicalVersion.components(separatedBy: .whitespacesAndNewlines)].flatMap { $0 }) + let tokens: Set = Set(trimmedSearchText.components(separatedBy: .whitespacesAndNewlines)) + wordFound = searchFields.intersection(tokens).count + } + + if wordFound == 2 { + if let first = tokens.pairs().first(where: { a,b in + label.contains(a) && label.contains(b) + }) { + return 2 + first.0.count + first.1.count + } + } else { + return wordFound * 10 + } + } else if let first = tokens.first { + if label.contains(first) { + return 1 + } + } + return 0 + } +} + struct FederalPlayer { var rank: Int var lastName: String @@ -91,4 +185,33 @@ struct FederalPlayer { club = result[10] } + static func lastRank(mostRecentDateAvailable: Date?, man: Bool) async -> Int? { + let context = PersistenceController.shared.localContainer.newBackgroundContext() + let lastPlayerFetch = ImportedPlayer.fetchRequest() + lastPlayerFetch.sortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: false)] + var predicate = NSPredicate(format: "male == \(man)") + if let mostRecentDateAvailable { + predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "importDate == %@", mostRecentDateAvailable as CVarArg)]) + } + lastPlayerFetch.predicate = predicate + + do { + if let lr = try context.fetch(lastPlayerFetch).first?.rank { + let fetch = ImportedPlayer.fetchRequest() + var rankPredicate = NSPredicate(format: "rank == %i", lr) + if let mostRecentDateAvailable { + rankPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [rankPredicate, NSPredicate(format: "importDate == %@", mostRecentDateAvailable as CVarArg)]) + } + fetch.predicate = rankPredicate + + let lastPlayersCount = try context.count(for: fetch) + return Int(lr) + Int(lastPlayersCount) - 1 + } + } catch { + print("ImportedPlayer.fetchRequest", error) + } + + return nil + } + } diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 35f0040..be16b6b 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -36,6 +36,10 @@ class GroupStage: ModelObject, Storable { self.startDate = startDate } + func tournamentObject() -> Tournament? { + Store.main.findById(tournament) + } + func title(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { case .wide: diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 80fac5b..cb3459e 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -43,6 +43,89 @@ class Match: ModelObject, Storable { self.order = order } + func title(_ displayStyle: DisplayStyle = .wide) -> String { + switch displayStyle { + case .wide: + return "Match \(index + 1)" + case .short: + return "#\(index + 1)" + } + } + + var matchFormat: MatchFormat { + get { + MatchFormat(rawValue: format ?? 0) ?? .defaultFormatForMatchType(.groupStage) + } + set { + format = newValue.rawValue + } + } + + func isReady() -> Bool { + teams().count == 2 + } + + func isEmpty() -> Bool { + teams().isEmpty + } + + func hasEnded() -> Bool { + endDate != nil + } + + func isGroupStage() -> Bool { + groupStage != nil + } + + func isTournamentMatch() -> Bool { + groupStageObject?.tournament != nil + } + + func walkoutTeam() -> [TeamRegistration] { + scores().filter({ $0.walkOut != nil }).compactMap { $0.team } + } + + func hasWalkoutTeam() -> Bool { + walkoutTeam().isEmpty == false + } + + func currentTournament() -> Tournament? { + groupStageObject?.tournamentObject() + } + + func scores() -> [TeamScore] { + Store.main.filter(isIncluded: { $0.match == id }) + } + + func teams() -> [TeamRegistration] { + scores().compactMap({ $0.team }).sorted(by: \.computedPosition) + } + + func teamWon(_ team: TeamData) -> Bool { + true + } + + func team(_ team: TeamData) -> TeamRegistration? { + switch team { + case .one: + teams().first + case .two: + teams().last + } + } + + func teamNames(_ team: TeamData) -> [String]? { + self.team(team)?.players().map { $0.lastName } + } + + func teamWalkOut(_ team: TeamData) -> Bool { + false + } + + func teamScore(_ team: TeamData) -> TeamScore? { + scores().first(where: { $0.teamRegistration == self.team(team)?.id }) + } + func isRunning() -> Bool { // at least a match has started hasStarted() && hasEnded() == false } @@ -64,16 +147,14 @@ class Match: ModelObject, Storable { // } } - func hasEnded() -> Bool { - endDate != nil - } - var roundObject: Round? { - Store.main.filter { $0.id == self.round }.first + guard let round else { return nil } + return Store.main.findById(round) } var groupStageObject: GroupStage? { - Store.main.filter { $0.id == self.groupStage }.first + guard let groupStage else { return nil } + return Store.main.findById(groupStage) } var isLoserBracket: Bool { diff --git a/PadelClub/Data/MockData.swift b/PadelClub/Data/MockData.swift index 37a06c7..84cfb85 100644 --- a/PadelClub/Data/MockData.swift +++ b/PadelClub/Data/MockData.swift @@ -32,3 +32,15 @@ extension Match { Match(index: 0, broadcasted: false, order: 0) } } + +extension TeamRegistration { + static func mock() -> TeamRegistration { + TeamRegistration(tournament: "") + } +} + +extension PlayerRegistration { + static func mock() -> PlayerRegistration { + PlayerRegistration(firstName: "Raz", lastName: "Sark", sex: 1) + } +} diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 8c45cad..2a65c7b 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -13,22 +13,101 @@ class PlayerRegistration: ModelObject, Storable { static func resourceName() -> String { "player-registrations" } var id: String = Store.randomId() - var teamRegistration: String + var teamRegistration: String? var firstName: String var lastName: String var licenceId: String? var rank: Int? - var hasPaid: Bool - var unranked: Bool + var registrationType: Int? + var registrationDate: Date? + var sex: Int + + var tournamentPlayed: Int? + var points: Double? + var clubName: String? + var ligueName: String? + var assimilation: String? + +// var phoneNumber: String? +// var email: String? +// var birthDate: Date? +// var club: String? - internal init(teamRegistration: String, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, hasPaid: Bool, unranked: Bool) { + 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 self.lastName = lastName self.licenceId = licenceId self.rank = rank - self.hasPaid = hasPaid - self.unranked = unranked + self.registrationType = registrationType + self.registrationDate = registrationDate + self.sex = sex + } + + internal init(importedPlayer: ImportedPlayer) { + self.teamRegistration = "" + self.firstName = importedPlayer.firstName ?? "" + self.lastName = importedPlayer.lastName ?? "" + self.licenceId = importedPlayer.license ?? nil + self.rank = Int(importedPlayer.rank) + self.sex = importedPlayer.male ? 1 : 0 + self.tournamentPlayed = importedPlayer.tournamentPlayed + self.points = importedPlayer.getPoints() + self.clubName = importedPlayer.clubName + self.ligueName = importedPlayer.ligueName + self.assimilation = importedPlayer.assimilation + } + + func tournament() -> Tournament? { + guard let tournament = team()?.tournament else { return nil } + return Store.main.findById(tournament) + } + + func team() -> TeamRegistration? { + guard let teamRegistration else { return nil } + return Store.main.findById(teamRegistration) + } + + func hasPaid() -> Bool { + registrationType != nil + } + + var paymentType: PaymentType { + get { + PaymentType(rawValue: registrationType ?? -1) ?? .notPaid + } + set { + registrationType = newValue.rawValue + } + } + + func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String { + lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized + } + + func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String { + if let rank, rank > 0 { + return rank.ordinalFormatted() + } else { + return "non classé" + (isMalePlayer() ? "" : "e") + } + } + + var computedRank: Int { + rank ?? tournament()?.unrankValue(for: isMalePlayer()) ?? Int.max + } + + func rank(for tournamentCategory: TournamentCategory, manMax: Int, womanMax: Int) -> Int { + switch tournamentCategory { + case .men: + return isMalePlayer() ? computedRank : computedRank + PlayerRegistration.addon(for: computedRank, manMax: manMax, womanMax: womanMax) + case .women, .mix: + return computedRank + } + } + + func isMalePlayer() -> Bool { + sex == 1 } enum CodingKeys: String, CodingKey { @@ -38,9 +117,105 @@ class PlayerRegistration: ModelObject, Storable { case _lastName = "lastName" case _licenceId = "licenceId" case _rank = "rank" - case _hasPaid = "hasPaid" - case _unranked = "unranked" - + case _registrationType = "registrationType" + case _registrationDate = "registrationDate" + case _sex = "sex" + case _tournamentPlayed = "tournamentPlayed" + case _points = "points" + case _clubName = "clubName" + case _ligueName = "ligueName" + case _assimilation = "assimilation" + } + + enum PaymentType: Int, CaseIterable, Identifiable { + var id: Self { + self + } + case notPaid = -1 + case cash = 0 + case lydia = 1 + case gift = 2 + case check = 3 + case paylib = 4 + var localizedLabel: String { + switch self { + case .notPaid: + return "Non réglé" + case .check: + return "Chèque" + case .cash: + return "Cash" + case .lydia: + return "Lydia" + case .paylib: + return "Paylib" + case .gift: + return "Offert" + } + } + } + + static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int { + switch playerRank { + case 0: return 0 + case womanMax: return manMax - womanMax + case manMax: return 0 + case 1...10: return 400 + case 11...30: return 1000 + case 31...60: return 2000 + case 61...100: return 3000 + case 101...200: return 8000 + case 201...500: return 12000 + default: + return 15000 + } + } + +} + +extension PlayerRegistration: Hashable { + static func == (lhs: PlayerRegistration, rhs: PlayerRegistration) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension PlayerRegistration: PlayerHolder { + + func getFirstName() -> String { + firstName + } + + func getLastName() -> String { + lastName + } + + func getPoints() -> Double? { + self.points + } + + func getRank() -> Int? { + rank + } + + func isUnranked() -> Bool { + rank == nil + } + + func formattedRank() -> String { + self.rankLabel() + } + + func formattedLicense() -> String { + if let licenceId { return licenceId.computedLicense } + return "aucune licence" + } + + var male: Bool { + isMalePlayer() } } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 23e6820..7b316de 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -39,16 +39,93 @@ class TeamRegistration: ModelObject, Storable { self.name = name } + var computedPosition: Int { + groupStagePosition ?? -1 + } + + func updatePlayers(_ players: Set) { + self.players().forEach { player in + if players.contains(player) == false { + try? DataStore.shared.playerRegistrations.delete(instance: player) + } + } + + players.forEach { player in + player.teamRegistration = id + try? DataStore.shared.playerRegistrations.addOrUpdate(instance: player) + } + + try? DataStore.shared.teamRegistrations.addOrUpdate(instance: self) + } + func qualified() -> Bool { groupStagePosition != nil && bracketPosition != nil } - var playerRegistrations: [PlayerRegistration] { - Store.main.filter { $0.teamRegistration == self.id } + typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool + + func players() -> [PlayerRegistration] { + Store.main.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in + let predicates: [AreInIncreasingOrder] = [ + { $0.sex < $1.sex }, + { $0.computedRank < $1.computedRank }, + { $0.lastName < $1.lastName}, + { $0.firstName < $1.firstName } + ] + + for predicate in predicates { + if !predicate(lhs, rhs) && !predicate(rhs, lhs) { + continue + } + + return predicate(lhs, rhs) + } + + return false + } + } + + func computedRank() -> Int { + (players().prefix(significantPlayerCount()).map { $0.computedRank } + missing().map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount()).reduce(0,+) + } + + func significantPlayerCount() -> Int { + tournamentObject()?.significantPlayerCount() ?? 2 + } + + func mandatoryPlayerType() -> [Int] { + guard let tournamentCategory = tournamentObject()?.tournamentCategory else { return [] } + switch tournamentCategory { + case .mix: + return [0, 1] + case .women: + return [0, 0] + case .men: + return [1, 1] + } + } + + func missing() -> [Int] { + let s = players().map { $0.sex } + var missing = mandatoryPlayerType() + s.forEach { i in + if let index = missing.firstIndex(of: i) { + missing.remove(at: index) + } + } + return missing + } + + func unrankValue(for malePlayer: Bool) -> Int { + tournamentObject()?.unrankValue(for: malePlayer) ?? Int.max + } + + func tournamentObject() -> Tournament? { + Store.main.findById(tournament) } override func deleteDependencies() throws { - try Store.main.deleteDependencies(items: self.playerRegistrations) + try Store.main.deleteDependencies(items: self.players()) } enum CodingKeys: String, CodingKey { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index f51a966..23cfb3c 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -41,14 +41,16 @@ class Tournament : ModelObject, Storable { var qualifiedPerGroupStage: Int var teamsPerGroupStage: Int var entryFee: Double? - + var maleUnrankedValue: Int? + var femaleUnrankedValue: Int? + @ObservationIgnored var navigationPath: [Screen] = [] @ObservationIgnored var undoManager: Int = 0 - internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil) { + internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, maleUnrankedValue: Int? = nil, femaleUnrankedValue: Int? = nil) { self.event = event self.creator = creator self.name = name @@ -77,6 +79,8 @@ class Tournament : ModelObject, Storable { self.qualifiedPerGroupStage = qualifiedPerGroupStage self.teamsPerGroupStage = teamsPerGroupStage self.entryFee = entryFee + self.maleUnrankedValue = maleUnrankedValue + self.femaleUnrankedValue = femaleUnrankedValue } enum State { @@ -85,24 +89,57 @@ class Tournament : ModelObject, Storable { } func state() -> Tournament.State { - if groupStageCount > 0 && groupStages.isEmpty == false { + if groupStageCount > 0 && groupStages().isEmpty == false { return .build } return .initial } - var groupStages: [GroupStage] { + func groupStages() -> [GroupStage] { Store.main.filter { $0.tournament == self.id }.sorted(by: \.index) } - var teamRegistrations: [TeamRegistration] { - Store.main.filter { $0.tournament == self.id } + 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() + } + } + } + + func players() -> [PlayerRegistration] { + teams().flatMap { $0.players() } + } + + func femalePlayers() -> [PlayerRegistration] { + players().filter({ $0.isMalePlayer() == false }) + } + + func unrankValue(for malePlayer: Bool) -> Int? { + switch tournamentCategory { + case .men: + return maleUnrankedValue + case .women: + return femaleUnrankedValue + case .mix: + return malePlayer ? maleUnrankedValue : femaleUnrankedValue + } } var rounds: Int { 4 } + func significantPlayerCount() -> Int { + 2 + } + + func missingUnrankedValue() -> Bool { + maleUnrankedValue == nil || femaleUnrankedValue == nil + } + func title(_ displayStyle: DisplayStyle = .wide) -> String { [tournamentLevel.localizedLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), name].compactMap({ $0 }).joined(separator: " ") } @@ -126,7 +163,7 @@ class Tournament : ModelObject, Storable { func qualifiedTeams() -> [TeamRegistration] { - teamRegistrations.filter({ $0.qualified() }) + teams().filter({ $0.qualified() }) } func moreQualifiedToDraw() -> Int { @@ -135,7 +172,7 @@ class Tournament : ModelObject, Storable { func missingQualifiedFromGroupStages() -> [TeamRegistration] { if groupStageAdditionalQualified > 0 { - return groupStages.filter { $0.hasEnded() }.compactMap { groupStage in + return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in groupStage.teams[qualifiedPerGroupStage] } .filter({ $0.qualified() == false }) @@ -145,7 +182,7 @@ class Tournament : ModelObject, Storable { } func groupStagesAreOver() -> Bool { - guard groupStages.isEmpty == false else { + guard groupStages().isEmpty == false else { return true } return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified @@ -153,7 +190,7 @@ class Tournament : ModelObject, Storable { func groupStageStatus() -> String { - let runningGroupStages = groupStages.filter({ $0.isRunning() }) + let runningGroupStages = groupStages().filter({ $0.isRunning() }) if groupStagesAreOver() { return "terminées" } if runningGroupStages.isEmpty { @@ -161,7 +198,7 @@ class Tournament : ModelObject, Storable { if ongoingGroupStages.isEmpty == false { return "Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { $0.index.formatted() }.joined(separator: ", ") + " en cours" } - return groupStages.count.formatted() + " poule" + groupStages.count.pluralSuffix + return groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix } else { return "Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { $0.index.formatted() }.joined(separator: ", ") + " en cours" } @@ -181,7 +218,7 @@ class Tournament : ModelObject, Storable { } func buildGroupStages() { - groupStages.forEach { groupStage in + groupStages().forEach { groupStage in try? DataStore.shared.groupStages.delete(instance: groupStage) } @@ -193,7 +230,7 @@ class Tournament : ModelObject, Storable { try? DataStore.shared.groupStages.append(contentOfs: _groupStages) - groupStages.forEach { $0.buildMatches() } + groupStages().forEach { $0.buildMatches() } refreshBrackets() } @@ -225,13 +262,13 @@ class Tournament : ModelObject, Storable { } func setBrackets(randomize: Bool) { - let numberOfBracketsAsInt = groupStages.count + let numberOfBracketsAsInt = groupStages().count // let teamsPerBracket = Int(teamsPerBracket) if groupStageCount != numberOfBracketsAsInt { buildGroupStages() return } - let max = groupStages.map { $0.size }.reduce(0,+) + 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 { @@ -253,6 +290,15 @@ class Tournament : ModelObject, Storable { func isFree() -> Bool { entryFee == nil || entryFee == 0 } + + func addTeam(_ players: Set) { + let team = TeamRegistration(tournament: id, registrationDate: Date()) + try? DataStore.shared.teamRegistrations.addOrUpdate(instance: team) + players.forEach { player in + player.teamRegistration = team.id + } + try? DataStore.shared.playerRegistrations.append(contentOfs: players) + } var teamSortingType: TeamSortingType { get { @@ -392,6 +438,8 @@ extension Tournament { case _qualifiedPerGroupStage = "qualifiedPerGroupStage" case _teamsPerGroupStage = "teamsPerGroupStage" case _entryFee = "entryFee" + case _maleUnrankedValue = "maleUnrankedValue" + case _femaleUnrankedValue = "femaleUnrankedValue" } } @@ -430,5 +478,7 @@ extension Tournament: Hashable { hasher.combine(qualifiedPerGroupStage) hasher.combine(teamsPerGroupStage) hasher.combine(entryFee) + hasher.combine(maleUnrankedValue) + hasher.combine(femaleUnrankedValue) } } diff --git a/PadelClub/Extensions/Sequence+Extensions.swift b/PadelClub/Extensions/Sequence+Extensions.swift index 5de1531..e97bea2 100644 --- a/PadelClub/Extensions/Sequence+Extensions.swift +++ b/PadelClub/Extensions/Sequence+Extensions.swift @@ -14,3 +14,9 @@ extension Sequence { } } } + +extension Sequence { + func pairs() -> AnySequence<(Element, Element)> { + AnySequence(zip(self, self.dropFirst())) + } +} diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index e1e96fe..9459e65 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -20,3 +20,83 @@ extension String { trimmed.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ").folding(options: .diacriticInsensitive, locale: .current).lowercased() } } + +extension String { + var computedLicense: String { + if let licenseKey { + return self + licenseKey + } else { + return self + } + } + + var strippedLicense: String? { + var dropFirst = 0 + if hasPrefix("0") { + dropFirst = 1 + } + if let match = self.dropFirst(dropFirst).firstMatch(of: /[0-9]{6,8}/) { + let lic = String(self.dropFirst(dropFirst)[match.range.lowerBound..= "I" { + value += 1 + if let newS = UnicodeScalar(i + value) { + c = Character(newS) + } + } + + if c >= "O" { + value += 1 + if let newS = UnicodeScalar(i + value) { + c = Character(newS) + } + } + + + if c >= "Q" { + value += 1 + if let newS = UnicodeScalar(i + value) { + c = Character(newS) + } + } + + return String(c) + } + } + return nil + } +} + +extension String { + func licencesFound() -> [String] { + let matches = self.matches(of: /[1-9][0-9]{5,7}/) + return matches.map { String(self[$0.range]) } + } +} diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index 4b61106..f04aade 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -801,11 +801,20 @@ enum TeamData: Int, Hashable, Codable, CaseIterable { } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { - switch self { - case .one: - return "#1" - case .two: - return "#2" + var shortName: String { + switch self { + case .one: + return "#1" + case .two: + return "#2" + } + } + + switch displayStyle { + case .wide: + return "Équipe " + shortName + case .short: + return shortName } } } diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index 08359b2..aeb3039 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -113,11 +113,11 @@ class SearchViewModel: ObservableObject, Identifiable { } func words() -> [String] { - searchText.trimmed.components(separatedBy: .whitespaces) + searchText.canonicalVersion.trimmed.components(separatedBy: .whitespaces) } func wordsPredicates() -> NSPredicate? { - let words = words() + let words = words().filter({ $0.isEmpty }) switch words.count { case 2: let predicates = [ @@ -132,7 +132,7 @@ class SearchViewModel: ObservableObject, Identifiable { func orPredicate() -> NSPredicate? { var predicates : [NSPredicate] = [] - + let searchText = searchText.canonicalVersion switch tokens.first { case .none: if searchText.isEmpty == false { diff --git a/PadelClub/Views/Club/ClubsView.swift b/PadelClub/Views/Club/ClubsView.swift index 4bf53ac..5725474 100644 --- a/PadelClub/Views/Club/ClubsView.swift +++ b/PadelClub/Views/Club/ClubsView.swift @@ -26,7 +26,7 @@ struct ClubsView: View { Button(role: .destructive) { try? dataStore.clubs.delete(instance: club) } label: { - Label("Effacer", systemImage: "trash") + LabelDelete() } } } diff --git a/PadelClub/Views/Components/Labels.swift b/PadelClub/Views/Components/Labels.swift index 807a275..269c18d 100644 --- a/PadelClub/Views/Components/Labels.swift +++ b/PadelClub/Views/Components/Labels.swift @@ -24,3 +24,9 @@ struct LabelSettings: View { Label("Réglages", systemImage: "slider.horizontal.3") } } + +struct LabelDelete: View { + var body: some View { + Label("Effacer", systemImage: "trash") + } +} diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index 7ac0d7d..f21ef4a 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -22,7 +22,7 @@ struct GroupStagesView: View { case -1: return "Toutes les poules" default: - return tournament.groupStages[selectedGroupStageIndex].title() + return tournament.groupStages()[selectedGroupStageIndex].title() } } // @@ -157,14 +157,13 @@ struct GroupStagesView: View { // } // } - ForEach(tournament.groupStages) { groupStage in + ForEach(tournament.groupStages()) { groupStage in if displayGroupStage(groupStage) && groupStage.hasEnded() == false { GroupStageView(groupStage: groupStage) if groupStage.matches.isEmpty == false { Section { ForEach(groupStage.matches) { match in - MatchRowView(setupSeedContext: false, matchViewStyle: .sectionedStandardStyle) - .environment(match) + MatchRowView(match: match, setupSeedContext: false, matchViewStyle: .sectionedStandardStyle) } } header: { Text("Matchs de la " + groupStage.title()) @@ -175,10 +174,10 @@ struct GroupStagesView: View { } .toolbar { ToolbarItem(placement: .principal) { - if tournament.groupStages.count < 6 { + if tournament.groupStages().count < 6 { Picker(selection: $selectedGroupStageIndex) { Text("Toutes").tag(-1) - ForEach(tournament.groupStages) { groupStage in + ForEach(tournament.groupStages()) { groupStage in Text(groupStage.title(.short)).tag(groupStage.index) } } label: { @@ -186,10 +185,10 @@ struct GroupStagesView: View { } .labelsHidden() .pickerStyle(.segmented) - } else if tournament.groupStages.count < 8 { + } else if tournament.groupStages().count < 8 { Picker(selection: $selectedGroupStageIndex) { Image(systemName: "square.stack").tag(-1) - ForEach(tournament.groupStages) { groupStage in + ForEach(tournament.groupStages()) { groupStage in Text(groupStage.title(.short)).tag(groupStage.index) } } label: { @@ -200,11 +199,11 @@ struct GroupStagesView: View { } else { Picker(selection: $selectedGroupStageIndex) { Text("Voir toutes les poules").tag(-1) - ForEach(tournament.groupStages) { groupStage in + ForEach(tournament.groupStages()) { groupStage in Text(groupStage.title()).tag(groupStage.index) } } label: { - Text("\(tournament.groupStages.count.formatted()) poules") + Text("\(tournament.groupStages().count.formatted()) poules") } } } diff --git a/PadelClub/Views/Match/MatchDateView.swift b/PadelClub/Views/Match/MatchDateView.swift new file mode 100644 index 0000000..a8c3185 --- /dev/null +++ b/PadelClub/Views/Match/MatchDateView.swift @@ -0,0 +1,110 @@ +// +// MatchDateView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 25/11/2023. +// + +import SwiftUI + +struct MatchDateView: View { + var match: Match + var showPrefix: Bool = false + + var body: some View { + Menu { + if match.startDate == nil { + Button("Commencer") { + match.startDate = Date() + save() + } + Button("Échauffement") { + match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) + save() + } + } else { + Button("Recommencer") { + match.startDate = Date() + match.endDate = nil + save() + } + Button("Remise à zéro") { + match.startDate = nil + match.endDate = nil + save() + } + } + } label: { + label + } + .buttonStyle(.plain) + } + + @ViewBuilder + var label: some View { + HStack { + VStack(alignment: .trailing) { + if match.hasWalkoutTeam() == false { + if let startDate = match.startDate, match.endDate == nil { + if startDate.timeIntervalSinceNow < 0 { + if showPrefix { + Text("en cours").font(.footnote).foregroundStyle(.secondary) + } + Text(startDate, style: .timer) + .monospacedDigit() + } else if startDate.timeIntervalSinceNow <= 7200 && showPrefix { + if showPrefix { + Text("démarre dans") + .font(.footnote).foregroundStyle(.secondary) + } + Text(startDate, style: .timer) + .monospacedDigit() + } else { + if showPrefix { + Text("le " + startDate.formatted(date: .abbreviated, time: .omitted)) + .font(.footnote).foregroundStyle(.secondary) + Text("à " + startDate.formatted(date: .omitted, time: .shortened)) + .monospacedDigit() + } else { + Text(startDate.formatted(date: .abbreviated, time: .shortened)) + .monospacedDigit() + } + } + } + if let startDate = match.startDate, let endDate = match.endDate { + let duration = Duration( + secondsComponent: Int64(endDate.timeIntervalSince(startDate)), + attosecondsComponent: 0 + ).formatted(.units(allowed: [.hours, .minutes], width: .narrow)) + if showPrefix { + Text("durée").font(.footnote).foregroundStyle(.secondary) + } + Text(duration) + .monospacedDigit() + } + + if match.startDate == nil { + Text("démarrage").font(.footnote).foregroundStyle(.secondary) + Text("non défini") + } + } + } + } + } + + func save() { + do { +// match.currentTournament?.objectWillChange.send() +// match.objectWillChange.send() +// try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + + +} + diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index a11137d..bbdd3c6 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -8,16 +8,534 @@ import SwiftUI struct MatchDetailView: View { - @Environment(Match.self) var match: Match - let setupSeedContext: Bool + @Environment(\.dismiss) var dismiss let matchViewStyle: MatchViewStyle + + @State private var showLiveScore: Bool = false + @State private var editScore: Bool = false + @State private var scoreType: ScoreType? + @State private var shareStat: Bool = false + @State private var startDateSetup: MatchDateSetup = .now + @State private var fieldSetup: MatchFieldSetup = .random + @State private var broadcasted: Bool = false + @State private var startDate: Date = Date() + @State private var endDate: Date = Date() + @State private var isEditing: Bool = false + @State private var showDetails: Bool = false + var match: Match + + init(match: Match, matchViewStyle: MatchViewStyle = .standardStyle) { + self.match = match + self.matchViewStyle = matchViewStyle + + if match.hasStarted() == false && (match.startDate == nil || match.court == nil) { + _isEditing = State(wrappedValue: true) + } + + if let startDate = match.startDate { + _startDateSetup = State(wrappedValue: .customDate) + _startDate = State(wrappedValue: startDate) + } else if match.isReady() == false { + _startDateSetup = State(wrappedValue: .customDate) + } + + if let endDate = match.endDate { + _endDate = State(wrappedValue: endDate) + } + + if let court = match.court { + _fieldSetup = State(wrappedValue: .field(court)) + } + } + +// @ViewBuilder +// func entrantView(_ entrant: Entrant) -> some View { +// Section { +// ForEach(entrant.orderedPlayers) { player in +// if player.isPlaying(in: match) { +// playerView(player) +// } +// } +// } header: { +// LabeledContent { +// if let tournament = match.currentTournament, let index = tournament.indexOfEntrant(entrant) { +// Text("#\(index + 1)") +// } +// } label: { +// if let title = entrant.brand?.title { +// Text(title) +// } +// } +// } footer: { +// LabeledContent { +// let weight = entrant.orderedPlayers.filter { $0.isPlaying(in: match) }.map { $0.tournamentRank }.reduce(0, +) +// Text(weight.formatted()) +// } label: { +// Text("Poids de la paire") +// } +// } +// .headerProminence(.increased) +// } + +// @ViewBuilder +// func playerView(_ player: Player) -> some View { +// VStack(alignment: .leading) { +// HStack { +// Text(player.longLabel) +// Text(player.localizedAge) +// Spacer() +// Text(player.formattedRank) +// } +// +// if let computedClubName = player.computedClubName { +// Text(computedClubName).foregroundStyle(.secondary).font(.caption) +// } +// if let computedLicense = player.computedLicense { +// Text(computedLicense).foregroundStyle(.secondary).font(.caption) +// } +// } +// } + var quickLookHeader: some View { + Section { + HStack { + if match.hasEnded() == false { + Menu { + Button("Non défini") { + match.court = nil + save() + } +// ForEach(1...match.numberOfField, id: \.self) { courtIndex in +// Button("Terrain #\(courtIndex.formatted())") { +// match.fieldIndex = Int64(courtIndex) +// save() +// } +// } + } label: { + VStack(alignment: .leading) { + Text("terrain").font(.footnote).foregroundStyle(.secondary) + if let court = match.court { + Text("#" + court) + } else { + Text("Choisir") + } + } + } + .buttonStyle(.plain) + } + Spacer() + MatchDateView(match: match, showPrefix: true) + } + .font(.title) + } footer: { +// if match.hasWalkoutTeam() == false { +// if let weatherData = match.weatherData { +// HStack { +// WeatherView(weatherData: weatherData) +// } +// } +// } + } + } + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + List { + if match.hasWalkoutTeam() == false { + if match.hasStarted() { + quickLookHeader + } else { + startingOptionView + } + } + + Section { + MatchSummaryView(match: match, matchViewStyle: .plainStyle) + } header: { + } footer: { + HStack { + if match.isTournamentMatch() { + if match.isEmpty() == false { + Button { + showDetails = true + } label: { + Text("Détails des joueurs") + } + } + } + Spacer() + if match.isEmpty() == false && match.isTournamentMatch() { + Menu { + //MenuWarnView(warningSender: match) + } label: { + Text("Prévenir") + } + .buttonStyle(.borderless) + } + } + } + + Section { + ForEach(match.teams()) { team in + ForEach(team.players().filter({ $0.hasPaid() == false })) { player in + HStack { + Text(player.playerLabel()) + Spacer() + //PlayerPayView(player: player) + } + } + } + } + + + menuView + } +// .sheet(isPresented: $showDetails) { +// NavigationStack { +// List { +// if let entrantOne = match.entrantOne() { +// entrantView(entrantOne) +// } +// if let entrantTwo = match.entrantTwo() { +// entrantView(entrantTwo) +// } +// } +// } +// .presentationDetents([.fraction(0.66)]) +// } +// .sheet(item: $scoreType, onDismiss: { +// if match.hasEnded() && match.isTournamentMatch() { +// dismiss() +// } +// }) { scoreType in +// switch scoreType { +// case .edition: +// let matchDescriptor = MatchDescriptor(match: match) +// EditScoreView(matchDescriptor: matchDescriptor) +// case .live: +// if let score = match.score { +// if score.sets.isEmpty { +// SplashView(score: score) +// } else { +// NewLiveScoringView(score: score) +// } +// } +// case .prepare: +// if match.freeMatchTeams.isEmpty == false { +// EditFreeMatchView(match: match) +// } else { +// PrepareMatchView(match: match) +// } +// case .stat: +// if let score = match.score { +// MatchStatView() +// .environmentObject(score) +// } +// case .health: +// HealthKitView(match: match) +// .presentationDetents([.medium]) +// case .feeling: +// if let feedbackData = match.feedbackData { +// FeedbackView(feedbackData: feedbackData) +// } +// } +// +// } + +// .refreshable { +// if match.isBroadcasted() { +// match.refreshBroadcast() +// } +// } +// .toolbar { +// ToolbarItem(placement: .topBarTrailing) { +// Menu { +// Button { +// scoreType = .live +// } label: { +// Label("Saisie Live", systemImage: "airplayaudio.circle") +// } +// +// Button { +// scoreType = .prepare +// } label: { +// Label("Préparer", systemImage: "calendar") +// } +// +// Divider() +// Menu { +// if match.fieldIndex > 0 { +// Button(role: .destructive) { +// match.currentTournament?.removeField(match.fieldIndex) +// match.fieldIndex = 0 +// match.refreshBroadcast() +// save() +// } label: { +// Label("Supprimer le terrain", systemImage: "figure.run") +// } +// } +// Button(role: .destructive) { +// match.restartMatch() +// save() +// } label: { +// Label("Supprimer l'horaire", systemImage: "xmark.circle.fill") +// } +// +// Button(role: .destructive) { +// match.resetScore() +// save() +// } label: { +// Label("Supprimer les scores", systemImage: "xmark.circle.fill") +// } +// +// if match.isFederalTournament == false && match.isFriendlyMatch == false { +// Button(role: .destructive) { +// match.resetMatch() +// save() +// } label: { +// Label("Supprimer les équipes et les scores", systemImage: "xmark.circle.fill") +// } +// } +// } label: { +// Text("Éditer") +// } +// +// } label: { +// Label("Options", systemImage: "ellipsis.circle") +// } +// } +// } + .navigationTitle(match.title()) + .navigationBarTitleDisplayMode(.large) + } + + enum MatchDateSetup: Hashable, Identifiable { + case inMinutes(Int) + case now + case customDate + + var id: Int { hashValue } + } + + + enum MatchFieldSetup: Hashable, Identifiable { + case random +// case firstAvailable + case field(String) + + var id: Int { hashValue } + } + + + enum ScoreType: Int, Identifiable, Hashable { + var id: Int { + self.rawValue + } + case edition = 0 + case live = 1 + case prepare = 2 + case stat = 3 + case feeling = 4 + case health = 5 + } + + var entrantLabelOne: String { + return "match.longLabelTeamOne" + } + + var entrantLabelTwo: String { + return "match.longLabelTeamTwo" + } + + @ViewBuilder + var menuView: some View { + if match.isReady() { + Section { + inputScoreView + } + } + + broadcastView + + if match.hasStarted() { + Section { + editionView + } + } + + shareView + +// if let followUpMatch = match.followUpMatch { +// Section { +// MatchRowView(match: followUpMatch) +// } header: { +// Text("à suivre terrain \(match.fieldIndex)") +// } +// } + } + + var inputScoreView: some View { + RowButtonView(title: "Saisir les résultats", systemImage: "list.clipboard") { + scoreType = .edition + } } + + var editionView: some View { + DisclosureGroup(isExpanded: $isEditing) { + startingOptionView + } label: { + if match.isTournamentMatch() { + Text("Modifier l'horaire et le terrain") + } else { + Text("Horaires et terrain") + } + } + } + + var startingOptionView: some View { + Section { + + if match.hasEnded() == false { + Picker(selection: $startDateSetup) { + if match.isReady() { + Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) + Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) + Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration)) + Text("Tout de suite").tag(MatchDateSetup.now) + } + Text("À").tag(MatchDateSetup.customDate) + } label: { + Text("Horaire") + } + .onChange(of: startDateSetup, perform: { value in + switch startDateSetup { + case .customDate: + break + case .now: + startDate = Date() + case .inMinutes(let minutes): + startDate = Date().addingTimeInterval(Double(minutes) * 60) + } + }) + } + + if match.startDate != nil || startDateSetup == .customDate { + DatePicker(selection: $startDate) { + Label("Début", systemImage: "calendar").labelStyle(.titleOnly) + } + .datePickerStyle(.compact) + } + + if match.endDate != nil { + DatePicker(selection: $endDate) { + Label("Fin", systemImage: "calendar").labelStyle(.titleOnly) + } + .datePickerStyle(.compact) + } + + + Picker(selection: $fieldSetup) { + Text("Au hasard").tag(MatchFieldSetup.random) + //Text("Premier disponible").tag(MatchFieldSetup.firstAvailable) +// ForEach(1...match.numberOfField, id: \.self) { courtIndex in +// let fieldIndex = Int64(courtIndex) +// let fieldIsAvailable : Bool = match.currentTournament?.fieldIsAvailable(fieldIndex) ?? true +// Label("Terrain #\(courtIndex)", systemImage: match.isFieldPreferred(fieldIndex) ? "heart" : "").tag(MatchFieldSetup.field(courtIndex)) +// } + } label: { + Text("Choix du terrain") + } + .contextMenu { + NavigationLink { + //FieldDrawView(match: match) + } label: { + Text("Tirage au sort visuel") + } + } + +// if match.canBroadcast() == true { +// Picker(selection: $broadcasted) { +// Text("Oui").tag(true) +// Text("Non").tag(false) +// } label: { +// Text("Diffuser automatiquement") +// } +// } + + RowButtonView(title: "Valider") { + if match.hasEnded() == false { + match.startDate = startDate + + if match.isTournamentMatch() { +// switch fieldSetup { +// case .random: +// let field = match.freeFields.randomElement() ?? match.currentTournament?.freeFields.randomElement() ?? 1 +// match.setupFieldAndStartDateIfPossible(field) +// case .field(let courtIndex): +// let fieldIndex = Int64(courtIndex) +// match.setupFieldAndStartDateIfPossible(fieldIndex) +// } + } + } else { + match.startDate = startDate + if match.endDate != nil { + match.endDate = endDate + } + } + + if broadcasted { + broadcastAndSave() + } else { + save() + } + + isEditing.toggle() + + if match.hasStarted() == false { + dismiss() + } + } + } + } + + @ViewBuilder + var broadcastView: some View { + Section { +// if match.isBroadcasted() { +// RowButtonView(title: "Arrêter de diffuser") { +// match.stopBroadcast() +// save() +// } +// } else if match.canBroadcast() == true { +// RowButtonView(title: "Diffuser", systemImage: "airplayvideo") { +// broadcastAndSave() +// } +// } + } + } + + var shareView: some View { + NavigationLink { + //EditSharingView(match: match) + } label: { + Text("Partage sur les réseaux sociaux") + } + } + + + private func save() { + } + + private func broadcastAndSave() { + Task { + //try? await match.broadcast() + + await MainActor.run { + } + } + } + } #Preview { - MatchDetailView(setupSeedContext: false, matchViewStyle: .standardStyle) - .environment(Match.mock()) + MatchDetailView(match: Match.mock(), matchViewStyle: .standardStyle) } diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index 9e31259..e1f69c5 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -8,20 +8,19 @@ import SwiftUI struct MatchRowView: View { - @Environment(Match.self) var match: Match + var match: Match let setupSeedContext: Bool let matchViewStyle: MatchViewStyle @ViewBuilder var body: some View { if setupSeedContext { - MatchSummaryView(setupSeedContext: setupSeedContext, matchViewStyle: matchViewStyle) + MatchSetupView(match: match) } else { NavigationLink { - MatchDetailView(setupSeedContext: setupSeedContext, matchViewStyle: matchViewStyle) - .environment(match) + MatchDetailView(match: match, matchViewStyle: matchViewStyle) } label: { - MatchSummaryView(setupSeedContext: setupSeedContext, matchViewStyle: matchViewStyle) + MatchSummaryView(match: match, matchViewStyle: matchViewStyle) } //.modifier(BroadcastViewModifier(isBroadcasted: match.isBroadcasted())) } @@ -30,6 +29,5 @@ struct MatchRowView: View { #Preview { - MatchRowView(setupSeedContext: false, matchViewStyle: .standardStyle) - .environment(Match.mock()) + MatchRowView(match: Match.mock(), setupSeedContext: false, matchViewStyle: .standardStyle) } diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift new file mode 100644 index 0000000..bcffe9f --- /dev/null +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -0,0 +1,35 @@ +// +// MatchSetupView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 23/03/2024. +// + +import SwiftUI + +struct MatchSetupView: View { + var match: Match + + var body: some View { + HStack { + VStack(alignment: .leading) { + _teamView(match.team(.one), index: 0) + _teamView(match.team(.two), index: 1) + } + } + } + + @ViewBuilder + func _teamView(_ team: TeamRegistration?, index: Int) -> some View { + if let team { + TeamDetailView(team: team) + } else { + TeamPickerView(match: match, index: match.index*2 + 1 + index) + .disabled(match.groupStage != nil) + } + } +} + +#Preview { + MatchSetupView(match: Match.mock()) +} diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index d540945..1e2a7dc 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -8,17 +8,254 @@ import SwiftUI struct MatchSummaryView: View { - @Environment(Match.self) var match: Match - let setupSeedContext: Bool + var match: Match let matchViewStyle: MatchViewStyle + var entrantLabelOne: String { + "match.longLabelTeamOne" + } + + var entrantLabelTwo: String { + "match.longLabelTeamTwo" + } + + var color: Color { + matchViewStyle == .plainStyle ? Color(uiColor: .tertiaryLabel) : Color(uiColor: .secondaryLabel) + } + + var width: CGFloat { + matchViewStyle == .plainStyle ? 1 : 2 + } var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + matchSummaryView +// .contextMenu { +// ForEach(match.teamScores) { entrant in +// if let team = entrant.team, team.orderedPlayers.count > 2 { +// NavigationLink { +// PlayerPickerView(match: match, team: team) +// } label: { +// if let teamTitle = team.entrant?.brand?.title { +// Text(teamTitle).foregroundStyle(.secondary) +// } else { +// let index = match.orderedEntrants.firstIndex(where: { $0 == entrant }) ?? 0 +// Text("Équipe \(index + 1)") +// } +// if match.players(from: team).isEmpty { +// Text("Choisir la paire") +// } else { +// Text("Modifier la paire") +// } +// } +// } +// } +// } + } + + @ViewBuilder + var matchSummaryView: some View { + VStack(alignment: .leading) { + if matchViewStyle != .plainStyle { + HStack { + if match.isGroupStage() && matchViewStyle != .feedStyle { + if let groupStage = match.groupStageObject, matchViewStyle == .standardStyle { + Text(groupStage.title()) + } +// if let index = match.entrantOne()?.bracketPositions?.first, let index2 = match.entrantTwo()?.bracketPositions?.first { +// Text("#\(index) contre #\(index2)") +// } + } else if let currentTournament = match.currentTournament() { + if matchViewStyle == .feedStyle { + //tournamentHeaderView(currentTournament) + } else if matchViewStyle != .sectionedStandardStyle { + Text(match.title(.short)) + } + } + if matchViewStyle == .standardStyle || matchViewStyle == .sectionedStandardStyle + { + Spacer() + if let court = match.court, match.hasEnded() == false { + Spacer() + Text("Terrain \(court)") + } + } + } + .lineLimit(1) + } + + if matchViewStyle != .feedStyle { + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: matchViewStyle == .plainStyle ? 8 : 0) { + PlayerBlockView(match: match, team: .one, color: color, width: width) + .padding(matchViewStyle == .plainStyle ? 0 : 8) + if width == 1 { + Divider() + } else { + Divider().frame(height: width).overlay(color) + } + PlayerBlockView(match: match, team: .two, color: color, width: width) + .padding(matchViewStyle == .plainStyle ? 0 : 8) + } + } + .overlay { + if matchViewStyle != .plainStyle { + RoundedRectangle(cornerRadius: 8) + .stroke(color, lineWidth: 2) + } + } + + if matchViewStyle != .plainStyle { + HStack { + Spacer() + MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle ? true : false) + } + } + } + } + .padding(.vertical, matchViewStyle != .plainStyle ? 8 : 0) + .monospacedDigit() + } + + @ViewBuilder + func tournamentHeaderView(_ currentTournament: Tournament) -> some View { + VStack(alignment: .leading) { +// HStack { +// ZStack(alignment: .leading) { +// Text("Poule 9").font(.title3).bold().opacity(0) +// Text(currentTournament.tournamentLevel.localizedLabel) +// } +// if let tournamentTitle = currentTournament.title, tournamentTitle.isEmpty == false { +// Text(tournamentTitle) +// } +// +// Spacer() +// if let startDate = match.startDate { +// if let endDate = match.endDate { +// let duration = Duration( +// secondsComponent: Int64(endDate.timeIntervalSince(startDate)), +// attosecondsComponent: 0 +// ).formatted(.units(allowed: [.hours, .minutes], width: .narrow)) +// Text("durée") +// } else if startDate.timeIntervalSinceNow < 0 { +// Text("en cours") +// } else { +// Text("prévu à") +// } +// } else { +// if let endDate = match.endDate { +// Text("a fini à") +// } +// } +// } +// +// HStack { +// ZStack(alignment: .leading) { +// Text("Poule 9").opacity(0) +// Text(match.shortTitleLabel) +// } +// if let score = match.score, match.hasEnded() { +// Text(score.label) +// } else if match.fieldIndex > 0 { +// Text("terrain #\(match.fieldIndex)") +// } +// +// Spacer() +// if let startDate = match.startDate { +// if let endDate = match.endDate { +// let duration = Duration( +// secondsComponent: Int64(endDate.timeIntervalSince(startDate)), +// attosecondsComponent: 0 +// ).formatted(.units(allowed: [.hours, .minutes], width: .narrow)) +// Text(duration) +// } else if startDate.timeIntervalSinceNow < 0 { +// Text(startDate, style: .timer) +// } else { +// Text(startDate.formatted(date: .omitted, time: .shortened)) +// } +// } else { +// if let endDate = match.endDate { +// Text(endDate.formatted(.dateTime.hour().minute())) +// } +// } +// } +// .font(.title3) +// +// HStack { +// ZStack(alignment: .leading) { +// Text("Poule 9").font(.title3).bold().opacity(0) +// VStack { +// Text(currentTournament.tournamentCategory.localizedLabel) +// } +// } +// +// if let winnerEntrant = match.winnerEntrant { +// Text(winnerEntrant.longLabelPlayerOne) +// } else if match.isBracketMatch { +// Text(match.subtitleLabel) +// } else if let entrantOne = match.entrantOne()?.team?.firstPairLastNames { +// Text(entrantOne) +// } +// +// Spacer() +// if let startDate = match.startDate { +// if let endDate = match.endDate { +// let duration = Duration( +// secondsComponent: Int64(endDate.timeIntervalSince(startDate)), +// attosecondsComponent: 0 +// ).formatted(.units(allowed: [.hours, .minutes], width: .narrow)) +// Text(startDate.formatted(.dateTime.hour().minute())) +// } else if startDate.timeIntervalSinceNow < 0 { +// Text(startDate.formatted(.dateTime.hour().minute())) +// } else { +// Text(startDate.formatted(.dateTime.day().month())) +// } +// } else { +// if let endDate = match.endDate { +// Text("début") +// } +// } +// } +// .font(.caption) +// +// HStack { +// ZStack(alignment: .leading) { +// Text("Poule 9").font(.title3).bold().opacity(0) +// VStack { +// Text(currentTournament.tournamentAgeLabel) +// } +// } +// +// if let winnerEntrant = match.winnerEntrant { +// Text(winnerEntrant.longLabelPlayerTwo) +// } else if match.isBracketMatch { +// } else if let entrant = match.entrantTwo()?.team?.firstPairLastNames { +// Text(entrant) +// } +// +// Spacer() +// if let startDate = match.startDate { +// if let endDate = match.endDate { +// let duration = Duration( +// secondsComponent: Int64(endDate.timeIntervalSince(startDate)), +// attosecondsComponent: 0 +// ).formatted(.units(allowed: [.hours, .minutes], width: .narrow)) +// Text(startDate.formatted(.dateTime.day().month().year())) +// } else if startDate.timeIntervalSinceNow < 0 { +// Text(startDate.formatted(.dateTime.day().month().year())) +// } else { +// Text(startDate.formatted(.dateTime.year())) +// } +// } else { +// if let endDate = match.endDate { +// Text("non défini") +// } +// } +// } +// .font(.caption) + } } } #Preview { - MatchSummaryView(setupSeedContext: false, matchViewStyle: .standardStyle) - .environment(Match.mock()) + MatchSummaryView(match: Match.mock(), matchViewStyle: .standardStyle) } diff --git a/PadelClub/Views/Match/PlayerBlockView.swift b/PadelClub/Views/Match/PlayerBlockView.swift new file mode 100644 index 0000000..c8802bf --- /dev/null +++ b/PadelClub/Views/Match/PlayerBlockView.swift @@ -0,0 +1,87 @@ +// +// PlayerBlockView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 25/11/2023. +// + +import SwiftUI + +struct PlayerBlockView: View { + var match: Match + + let team: TeamData + let color: Color + let width: CGFloat + + var names: [String]? { + match.teamNames(team) + } + + var hasWon: Bool { + match.teamWon(team) + } + + var hideScore: Bool { + match.hasWalkoutTeam() + } + + var isWalkOut: Bool { + match.teamWalkOut(team) + } + + var scores: [String] { + match.teamScore(team)?.score?.components(separatedBy: ",") ?? [] + } + + private func _defaultLabel() -> String { + team.localizedLabel() + } + + var body: some View { + HStack { + VStack(alignment: .leading) { + if let names { + ForEach(names, id: \.self) { name in + Text(name).lineLimit(1) + } + } else { + ZStack(alignment: .leading) { + VStack { + Text("longLabelPlayerOne").lineLimit(1) + Text("longLabelPlayerTwo").lineLimit(1) + } + .opacity(0) + Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1) + } + } + } + .bold(hasWon) + Spacer() + if hasWon { + Image(systemName: "trophy") + } else if isWalkOut { + Text("WO") + } + + if hideScore == false { + ForEach(scores.indices, id: \.self) { index in + let string = scores[index] + if string.isEmpty == false { + if width == 1 { + Divider() + } else { + Divider().frame(width: width).overlay(color) + } + Text(string) + .font(.title3) + .frame(maxWidth: 20) + .scaledToFill() + .minimumScaleFactor(0.5) + .lineLimit(1) + } + } + } + } + } +} diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 9de1a2b..35688bb 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -15,6 +15,9 @@ struct MainView: View { @State private var checkingFiles: Bool = false @AppStorage("lastDataSource") var lastDataSource: String? + @AppStorage("lastDataSourceMaleUnranked") var lastDataSourceMaleUnranked: Int? + @AppStorage("lastDataSourceFemaleUnranked") var lastDataSourceFemaleUnranked: Int? + @Environment(\.managedObjectContext) private var viewContext @FetchRequest( @@ -123,10 +126,18 @@ struct MainView: View { importingFiles = true Task { lastDataSource = await FileImportManager.shared.importDataFromFFT() + if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) { + await _calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) + } importingFiles = false } } + private func _calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async { + lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true) + 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) diff --git a/PadelClub/Views/Player/Components/PlayerPopoverView.swift b/PadelClub/Views/Player/Components/PlayerPopoverView.swift new file mode 100644 index 0000000..b1b7a12 --- /dev/null +++ b/PadelClub/Views/Player/Components/PlayerPopoverView.swift @@ -0,0 +1,229 @@ +// +// PlayerPopoverView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 24/03/2024. +// + +import SwiftUI + +struct PlayerPopoverView: View { + enum PlayerCreationField { + case firstName, lastName, license, rank + } + + @Environment(\.dismiss) var dismiss + + @State private var displayWrongLicenceError: Bool = false + @State private var firstName: String = "" + @State private var lastName: String = "" + @State private var license: String = "" + @State private var rank: Int? + @State var sex: Int = 1 + + let requiredField: [PlayerCreationField] + let creationCompletionHandler: ((PlayerRegistration) -> Void) + + @FocusState private var firstNameIsFocused: Bool + @FocusState private var lastNameIsFocused: Bool + @FocusState private var licenseIsFocused: Bool + @FocusState private var amountIsFocused: Bool + + static var source: String? + + init(sex: Int, requiredField: [PlayerCreationField] = [.firstName, .lastName], creationCompletionHandler: @escaping (PlayerRegistration) -> Void) { + let source = PlayerPopoverView.source + if let source { + let words = source.components(separatedBy: .whitespaces) + if words.isEmpty == false { + _firstName = State(wrappedValue: words.first?.capitalized ?? "") + + if words.count > 1 { + _lastName = State(wrappedValue: words.last?.capitalized ?? "") + } + } else { + _firstName = State(wrappedValue: source) + } + } + + _sex = State(wrappedValue: sex) + _rank = State(wrappedValue: nil) + + self.requiredField = requiredField + self.creationCompletionHandler = creationCompletionHandler + self.pasteBoard = UIPasteboard.general.string + } + + @State private var pasteBoard: String? + + var body: some View { + NavigationStack { + List { + if let pasteBoard { + Section { + Text(pasteBoard).foregroundColor(.clear).padding(8) + .frame(maxWidth: .infinity) + .overlay( + TextEditor(text: .constant(pasteBoard)) + ) + .frame(minHeight: 20.0) + } header: { + HStack { + Spacer() + Button { + self.pasteBoard = "" + } label: { + Text("effacer le contenu du presse-papier") + } + .buttonStyle(.borderless) + } + } + .textCase(nil) + } + Section { + Picker(selection: $sex) { + Text("Homme").tag(1 as Int) + Text("Femme").tag(0 as Int) + } label: { + + } + .labelsHidden() + .pickerStyle(.segmented) + + HStack { + Text("Prénom").foregroundStyle(.secondary) + Spacer() + TextField("Prénom", text: $firstName) + .submitLabel(.next) + .focused($firstNameIsFocused) + .onSubmit { + firstName = firstName.trimmed + lastNameIsFocused = true + } + .fixedSize() + } + HStack { + Text("Nom").foregroundStyle(.secondary) + Spacer() + TextField("Nom", text: $lastName) + .focused($lastNameIsFocused) + .submitLabel(.next) + .onSubmit { + lastName = lastName.trimmed + licenseIsFocused = true + } + .fixedSize() + } + HStack { + Text("Licence").foregroundStyle(.secondary) + Spacer() + TextField("Licence", text: $license) + .focused($licenseIsFocused) + .keyboardType(.namePhonePad) + .submitLabel(.next) + .onSubmit { + license = license.trimmed + if requiredField.contains(.license) { + if license.isLicenseNumber { + amountIsFocused = true + } else { + displayWrongLicenceError = true + } + } else { + amountIsFocused = true + } + } + .fixedSize() + } + HStack { + Text("Rang").foregroundStyle(.secondary) + Spacer() + TextField("Non classé", value: $rank, format: .number) + .focused($amountIsFocused) + .keyboardType(.asciiCapable) + .submitLabel(.done) + .fixedSize() + } + } header: { + HStack { + Spacer() + Button { + let last = firstName + firstName = lastName + lastName = last + } label: { + Text("inverser nom & prénom") + }.buttonStyle(.borderless) + } + .textCase(nil) + } + .multilineTextAlignment(.trailing) + + Section { + RowButtonView(title: "Valider et ajouter un autre") { + createManualPlayer() + lastName = "" + firstName = "" + license = "" + rank = nil + firstNameIsFocused = true + } + } + } + .onAppear { + firstNameIsFocused = true + } + .autocorrectionDisabled() + .navigationTitle(sex == 1 ? "Nouveau joueur" : "Nouvelle joueuse") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Valider") { + createManualPlayer() + dismiss() + } + .clipShape(Capsule()) + .buttonStyle(.bordered) + + } + ToolbarItem(placement: .cancellationAction) { + Button("Annuler", role : .cancel) { + dismiss() + } + } + } + .alert("Attention", isPresented: $displayWrongLicenceError) { + Button("OK") { + + } + } message: { + Text("La licence n'est pas valide") + } + } + } + + func createManualPlayer() { + guard (lastName.isEmpty == false && requiredField.contains(.lastName)) || requiredField.contains(.lastName) == false else { + return + } + + guard (license.isEmpty == false && license.isLicenseNumber && requiredField.contains(.license)) || requiredField.contains(.license) == false else { + return + } + + guard (firstName.isEmpty == false && requiredField.contains(.firstName)) || requiredField.contains(.firstName) == false else { + return + } + + let playerRegistration = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: license.trimmed.isEmpty ? nil : license, rank: rank, sex: sex) + self.creationCompletionHandler(playerRegistration) + } + +} + +#Preview { + PlayerPopoverView(sex: 1) { player in + + } +} diff --git a/PadelClub/Views/Player/Components/PlayerSexPickerView.swift b/PadelClub/Views/Player/Components/PlayerSexPickerView.swift new file mode 100644 index 0000000..31a8752 --- /dev/null +++ b/PadelClub/Views/Player/Components/PlayerSexPickerView.swift @@ -0,0 +1,48 @@ +// +// PlayerSexPickerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 24/03/2024. +// + +import SwiftUI + +struct PlayerSexPickerView: View { + @Bindable var player: PlayerRegistration + + var body: some View { + HStack { + Text(player.playerLabel()) + Spacer() + Picker(selection: $player.sex) { + Text("Homme").tag(1 as Int64) + Text("Femme").tag(0 as Int64) + } label: { + + } + .pickerStyle(.segmented) + .fixedSize() + .onChange(of: player.sex) { + save() + } + } + } + + func save() { + do { +// player.objectWillChange.send() +// player.team?.entrant?.tournament?.objectWillChange.send() +// try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + +} + +#Preview { + PlayerSexPickerView(player: PlayerRegistration.mock()) +} diff --git a/PadelClub/Views/Player/PlayerView.swift b/PadelClub/Views/Player/PlayerView.swift new file mode 100644 index 0000000..e72a347 --- /dev/null +++ b/PadelClub/Views/Player/PlayerView.swift @@ -0,0 +1,28 @@ +// +// PlayerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 24/03/2024. +// + +import SwiftUI + +struct PlayerView: View { + @EnvironmentObject var dataStore: DataStore + let player: PlayerRegistration + + var body: some View { + ImportedPlayerView(player: player) + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button(role: .destructive) { + try? dataStore.playerRegistrations.delete(instance: player) + } label: { + LabelDelete() + } + } + } +} + +#Preview { + PlayerView(player: PlayerRegistration.mock()) +} diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index b5b46e8..c3ffb0c 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -8,17 +8,14 @@ import SwiftUI struct ImportedPlayerView: View { - let player: ImportedPlayer - var hideLigue: Bool = false - var hideClub: Bool = false - var hidePoints: Bool = false + let player: PlayerHolder var index: Int? = nil var body: some View { VStack(alignment: .leading) { HStack { - Text(player.lastName!.capitalized) - Text(player.firstName!.capitalized) + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) if index == nil { Text(player.male ? "♂︎" : "♀︎") } @@ -35,37 +32,43 @@ struct ImportedPlayerView: View { } } .font(.title3) + .lineLimit(1) HStack { HStack(alignment: .top, spacing: 0) { - Text(player.rank.formatted()).italic(player.isAssimilated) + Text(player.formattedRank()).italic(player.isAssimilated) .font(.title3) - Text(player.rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) .font(.caption) + } } - if hidePoints == false { - + if let pts = player.getPoints(), pts > 0 { HStack(alignment: .lastTextBaseline, spacing: 0) { - let pts = player.points - if pts > 0 { - Text(pts.formatted()).font(.title3) - Text(" pts").font(.caption) - } + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) } + } + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { HStack(alignment: .lastTextBaseline, spacing: 0) { - if player.tournamentCount > 0 { - Text(player.tournamentCount.formatted()).font(.title3) - Text(" tournoi" + player.tournamentCount.pluralSuffix).font(.caption) - } + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) } } } - Text(player.clubName!) - .font(.caption) - Text(player.ligueName!) + Text(player.formattedLicense()) .font(.caption) + + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } } } } diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 4214a34..5d75dd6 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -161,6 +161,30 @@ struct SelectablePlayerListView: View { Text("Annuler") } } + + if searchViewModel.selectedPlayers.isEmpty == false { + ToolbarItem(placement: .bottomBar) { + Button { + searchViewModel.filterSelectionEnabled.toggle() + } label: { + if searchViewModel.filterSelectionEnabled { + Image(systemName: "line.3.horizontal.decrease.circle.fill") + } else { + Image(systemName: "line.3.horizontal.decrease.circle") + } + } + } + ToolbarItem(placement: .status) { + Button { + if let playerSelectionAction { + playerSelectionAction(searchViewModel.selectedPlayers) + } + dismiss() + } label: { + Text("Ajouter le" + searchViewModel.selectedPlayers.count.pluralSuffix + " \(searchViewModel.selectedPlayers.count) joueur" + searchViewModel.selectedPlayers.count.pluralSuffix) + } + } + } } } // .modifierWithCondition(searchViewModel.user != nil) { thisView in diff --git a/PadelClub/Views/Team/TeamDetailView.swift b/PadelClub/Views/Team/TeamDetailView.swift new file mode 100644 index 0000000..757a898 --- /dev/null +++ b/PadelClub/Views/Team/TeamDetailView.swift @@ -0,0 +1,27 @@ +// +// TeamDetailView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 23/03/2024. +// + +import SwiftUI + +struct TeamDetailView: View { + @EnvironmentObject var dataStore: DataStore + var team: TeamRegistration + + var body: some View { + if team.players().isEmpty { + Text("Aucun joueur, espace réservé") + } else { + ForEach(team.players()) { player in + PlayerView(player: player) + } + } + } +} + +#Preview { + TeamDetailView(team: TeamRegistration.mock()) +} diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift new file mode 100644 index 0000000..712c37d --- /dev/null +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -0,0 +1,22 @@ +// +// TeamPickerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 23/03/2024. +// + +import SwiftUI + +struct TeamPickerView: View { + var match: Match + var index: Int + + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + TeamPickerView(match: Match.mock(), index: 0) + +} diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift new file mode 100644 index 0000000..617aa1f --- /dev/null +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -0,0 +1,21 @@ +// +// TeamRowView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 24/03/2024. +// + +import SwiftUI + +struct TeamRowView: View { + @EnvironmentObject var dataStore: DataStore + var team: TeamRegistration + + var body: some View { + TeamDetailView(team: team) + } +} + +#Preview { + TeamRowView(team: TeamRegistration.mock()) +} diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionTipsView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionTipsView.swift new file mode 100644 index 0000000..cfdca41 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionTipsView.swift @@ -0,0 +1,88 @@ +// +// InscriptionTipsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 24/03/2024. +// + +import SwiftUI +import TipKit + +struct InscriptionTipsView: View { + @Environment(Tournament.self) private var tournament: Tournament + + var body: some View { + List { + + Section { + + let fileTip = InscriptionManagerFileInputTip() + TipView(fileTip) { action in + if action.id == "website" { + } else if action.id == "add-team-file" { + } + } + .tipStyle(tint: nil) + } + + Section { + + let pasteTip = InscriptionManagerPasteInputTip() + TipView(pasteTip) { action in + if let paste = UIPasteboard.general.string { + //self.pasteField = paste + } + } + .tipStyle(tint: nil) + } + + Section { + + let searchTip = InscriptionManagerSearchInputTip() + TipView(searchTip) { action in + //presentPlayerCreation = true + } + .tipStyle(tint: nil) + } + + Section { + + let createTip = InscriptionManagerCreateInputTip() + TipView(createTip) { action in + //presentPlayerSelection = true + } + .tipStyle(tint: nil) + } + + Section { + 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 { + // + // if #available(iOS 17.0, *) { + // Section { + // let tip = InscriptionManagerRankUpdateTip() + // TipView(tip) { action in + // self.currentRankSourceDate = mostRecentDate + // } + // .tipStyle(tint: nil) + // } + // } + // + // rankingDateSourcePickerView(showDateInLabel: false) + // } else if tournament.currentRankSourceDate == nil { + // rankingDateSourcePickerView(showDateInLabel: false) + // } + // + + } + } +} + +#Preview { + InscriptionTipsView() + .environment(Tournament.mock()) +} diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index c75a128..6ff0801 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -6,14 +6,356 @@ // import SwiftUI +import TipKit struct InscriptionManagerView: View { - let tournament: Tournament + @EnvironmentObject var dataStore: DataStore - var body: some View { + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], + animation: .default) + private var fetchPlayers: FetchedResults + + var tournament: Tournament + @State private var searchField: String = "" + @State private var presentPlayerSearch: Bool = false + @State private var presentPlayerCreation: Bool = false + @State private var createdPlayers: Set = Set() + @State private var testCreatedPlayers: Set = Set() + @State private var editedTeam: TeamRegistration? + @State private var pasteString: String? + + let slideToDeleteTip = SlideToDeleteTip() + let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip() + + private func _pastePredicate(pasteField: String, mostRecentDate: Date?) -> NSPredicate? { + let text = pasteField.canonicalVersion + + let nameComponents = text.components(separatedBy: .whitespacesAndNewlines).compactMap { $0.isEmpty ? nil : $0 }.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 }) + var andPredicates = [NSPredicate]() + var orPredicates = [NSPredicate]() + //self.wordsCount = nameComponents.count + + + if _filterOption() == .male { + andPredicates.append(NSPredicate(format: "male == YES")) + } else if _filterOption() == .female { + andPredicates.append(NSPredicate(format: "male == NO")) + } + + if let mostRecentDate { + andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) + } + + if nameComponents.count > 1 { + orPredicates = nameComponents.pairs().map { + return NSPredicate(format: "(firstName contains[cd] %@ AND lastName contains[cd] %@) OR (firstName contains[cd] %@ AND lastName contains[cd] %@)", $0, $1, $1, $0) } + } else { + orPredicates = nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) } + } + + let matches = text.licencesFound() + let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) } + orPredicates = orPredicates + licensesPredicates + + var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates) + + if orPredicates.isEmpty == false { + predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates)]) + } + + return predicate + } + + private func _currentSelection() -> Set { + var currentSelection = Set() + testCreatedPlayers.compactMap { id in + fetchPlayers.first(where: { id == $0.license }) + }.forEach { player in + let player = PlayerRegistration(importedPlayer: player) + currentSelection.insert(player) + } + + testCreatedPlayers.compactMap { id in + createdPlayers.first(where: { id == $0.id }) + }.forEach { + currentSelection.insert($0) + } + return currentSelection + } + + private func _createTeam() { + tournament.addTeam(_currentSelection()) + createdPlayers.removeAll() + testCreatedPlayers.removeAll() + pasteString = nil + } + + private func _updateTeam() { + editedTeam?.updatePlayers(_currentSelection()) + createdPlayers.removeAll() + testCreatedPlayers.removeAll() + pasteString = nil + editedTeam = nil + } + + private func _buildingTeamView() -> some View { + List(selection: $testCreatedPlayers) { + Section { + ForEach(testCreatedPlayers.sorted(), id: \.self) { id in + if let p = createdPlayers.first(where: { $0.id == id }) { + PlayerView(player: p).tag(p.id) + } + if let p = fetchPlayers.first(where: { $0.license == id }) { + ImportedPlayerView(player: p).tag(p.license!) + } + } +// ForEach(createdPlayers.sorted(by: \.computedRank)) { player in +// PlayerView(player: player).tag(player.id) +// } + } + + if editedTeam == nil { + RowButtonView(title: "Ajouter l'équipe") { + _createTeam() + } + } else { + RowButtonView(title: "Modifier l'équipe") { + _updateTeam() + } + } + + 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: { + HStack { + Text("contenu du presse-papier") + Spacer() + Button("effacer", role: .destructive) { + self.pasteString = nil + self.createdPlayers.removeAll() + self.testCreatedPlayers.removeAll() + } + .buttonStyle(.borderless) + } + } + + 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) + } + } + } + .onReceive(fetchPlayers.publisher.count()) { _ in // <-- here + if let pasteString, count == 2 { + fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in + testCreatedPlayers.insert(player.license!) + } + } + } + + .environment(\.editMode, Binding.constant(EditMode.active)) + } + + var count: Int { + return fetchPlayers.filter { $0.hitForSearch(pasteString ?? "") >= hitTarget }.count + } + + var hitTarget: Int { + if (pasteString?.matches(of: /[1-9][0-9]{5,7}/).count ?? 0) > 1 { + if fetchPlayers.filter({ $0.hitForSearch(pasteString ?? "") == 100 }).count == 2 { return 100 } + } else { + return 2 + } + return 1 + } + + private func _teamRegisteredView() -> some View { List { - Text("24") + Section { + if tournament.tournamentCategory == .men && tournament.femalePlayers().isEmpty == false { + + TipView(inscriptionManagerWomanRankTip) + .tipStyle(tint: nil) + } + } header: { + Text("Informations") + } + + Section { + TipView(slideToDeleteTip) + .tipStyle(tint: nil) + } + + ForEach(tournament.teams()) { team in + Section { + TeamRowView(team: team) + } header: { + HStack { + Text("Équipe") + Spacer() + Text(team.computedRank().formatted()) + } + } footer: { + HStack { + Spacer() + Menu { + Button("Éditer") { + editedTeam = team + team.players().forEach { player in + createdPlayers.insert(player) + testCreatedPlayers.insert(player.id) + } + } + Divider() + Button(role: .destructive) { + try? dataStore.teamRegistrations.delete(instance: team) + } label: { + LabelDelete() + } + } label: { + LabelOptions() + } + } + } + .headerProminence(.increased) + } + } + .searchable(text: $searchField) + } + + var body: some View { + VStack(spacing: 0) { + _managementView() + if testCreatedPlayers.isEmpty == false || pasteString != nil || editedTeam != nil { + _buildingTeamView() + } else if tournament.teams().isEmpty { + InscriptionTipsView() + } else { + _teamRegisteredView() + } + } + .sheet(isPresented: $presentPlayerSearch) { + NavigationStack { + SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption()) { players in + players.forEach { player in + let newPlayer = PlayerRegistration(importedPlayer: player) + createdPlayers.insert(newPlayer) + testCreatedPlayers.insert(newPlayer.id) + } + } + } + } + .sheet(isPresented: $presentPlayerCreation) { + PlayerPopoverView(sex: _addPlayerSex()) { p in + createdPlayers.insert(p) + testCreatedPlayers.insert(p.id) + } } - .navigationTitle("Inscriptions") + .toolbar { + if testCreatedPlayers.isEmpty == false { + ToolbarItem(placement: .cancellationAction) { + Button("Annuler", role: .cancel) { + createdPlayers.removeAll() + testCreatedPlayers.removeAll() + } + } + } + } + .navigationBarBackButtonHidden(testCreatedPlayers.isEmpty == false) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Inscription") + .navigationBarTitleDisplayMode(.inline) + } + + private func _managementView() -> some View { + HStack { + Button { + presentPlayerCreation = true + } label: { + HStack(spacing: 4) { + Image(systemName: "person.fill.badge.plus") + .resizable() + .scaledToFit() + .frame(width: 20) + Text("Créer") + .font(.headline) + } + .frame(maxWidth: .infinity) + } + + PasteButton(payloadType: String.self) { strings in + guard let first = strings.first else { return } + Task { + await MainActor.run() { + fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: nil) + pasteString = first + } + } + } + + Button { + presentPlayerSearch = true + } label: { + HStack(spacing: 4) { + Image(systemName: "person.fill.viewfinder") + .resizable() + .scaledToFit() + .frame(width: 20) + Text("FFT") + .font(.headline) + } + .frame(maxWidth: .infinity) + } + } + .buttonStyle(.borderedProminent) + .tint(.launchScreenBackground) + .fixedSize(horizontal: false, vertical: true) + .padding(16) + } + + private func _addPlayerSex() -> Int { + switch tournament.tournamentCategory { + case .men: + return 1 + case .women: + return 0 + case .mix: + return 1 + } + + } + + private func _filterOption() -> PlayerFilterOption { + switch tournament.tournamentCategory { + case .men: + return .male + case .women: + return .female + case .mix: + return .all + } + } +} + +#Preview { + NavigationStack { + InscriptionManagerView(tournament: Tournament.mock()) + .environment(Tournament.mock()) } } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 2d6c296..3ad50b2 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -8,11 +8,40 @@ import SwiftUI struct TournamentView: View { + @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament var presentationContext: PresentationContext = .agenda - + @AppStorage("lastDataSource") var lastDataSource: String? + + var _lastDataSourceDate: Date? { + guard let lastDataSource else { return nil } + return URL.importDateFormatter.date(from: lastDataSource) + } + var body: some View { List { + + if tournament.missingUnrankedValue() { + Button("update NC") { + Task { + tournament.maleUnrankedValue = await FederalPlayer.lastRank(mostRecentDateAvailable: _lastDataSourceDate, man: true) + tournament.femaleUnrankedValue = await FederalPlayer.lastRank(mostRecentDateAvailable: _lastDataSourceDate, man: false) + + try? dataStore.tournaments.addOrUpdate(instance: tournament) + } + } + } + + + NavigationLink(value: Screen.inscription) { + LabeledContent { + Text(tournament.teams().count.formatted()) + } label: { + Text("Inscriptions") + } + + } + switch tournament.state() { case .initial: TournamentInitView()