From 455074c155d950a87552b46abe1136d070ccf867 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 17 Apr 2024 11:14:22 +0200 Subject: [PATCH] clean up cashier --- PadelClub.xcodeproj/project.pbxproj | 16 + PadelClub/Data/Federal/FederalPlayer.swift | 2 + PadelClub/Data/PlayerRegistration.swift | 20 + PadelClub/Data/Tournament.swift | 14 +- .../Views/Cashier/CashierDetailView.swift | 45 +- .../Views/Cashier/CashierSettingsView.swift | 56 ++ PadelClub/Views/Cashier/CashierView.swift | 592 ++++++------------ PadelClub/Views/Cashier/PlayerListView.swift | 18 + .../Components/EditablePlayerView.swift | 100 +++ .../Views/Shared/ImportedPlayerView.swift | 3 + .../Components/InscriptionInfoView.swift | 2 +- .../Screen/TournamentCashierView.swift | 100 +++ .../Views/Tournament/TournamentView.swift | 2 +- 13 files changed, 551 insertions(+), 419 deletions(-) create mode 100644 PadelClub/Views/Cashier/CashierSettingsView.swift create mode 100644 PadelClub/Views/Cashier/PlayerListView.swift create mode 100644 PadelClub/Views/Player/Components/EditablePlayerView.swift create mode 100644 PadelClub/Views/Tournament/Screen/TournamentCashierView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index c4f6018..ede2098 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -86,6 +86,10 @@ FF0EC5762BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5452BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv */; }; FF0EC5772BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC54A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv */; }; FF11627A2BCF8109000C4809 /* CallMessageCustomizationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */; }; + FF11627D2BCF941A000C4809 /* CashierSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11627C2BCF941A000C4809 /* CashierSettingsView.swift */; }; + FF11627F2BCF9432000C4809 /* PlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11627E2BCF9432000C4809 /* PlayerListView.swift */; }; + FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162802BCF945C000C4809 /* TournamentCashierView.swift */; }; + FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; @@ -356,6 +360,10 @@ FF0EC54B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-11-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-11-2022.csv"; sourceTree = ""; }; FF0EC54C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-12-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-12-2022.csv"; sourceTree = ""; }; FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageCustomizationView.swift; sourceTree = ""; }; + FF11627C2BCF941A000C4809 /* CashierSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierSettingsView.swift; sourceTree = ""; }; + FF11627E2BCF9432000C4809 /* PlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerListView.swift; sourceTree = ""; }; + FF1162802BCF945C000C4809 /* TournamentCashierView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentCashierView.swift; sourceTree = ""; }; + FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditablePlayerView.swift; sourceTree = ""; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = ""; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = ""; }; @@ -707,6 +715,7 @@ FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */, FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */, FF9267FB2BCE84870080F940 /* PlayerPayView.swift */, + FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */, ); path = Components; sourceTree = ""; @@ -774,6 +783,8 @@ children = ( FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */, FF9267F72BCE78C70080F940 /* CashierView.swift */, + FF11627E2BCF9432000C4809 /* PlayerListView.swift */, + FF11627C2BCF941A000C4809 /* CashierSettingsView.swift */, ); path = Cashier; sourceTree = ""; @@ -837,6 +848,7 @@ FF8F26532BAE1E4400650388 /* TableStructureView.swift */, FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */, FF9268062BCE94D90080F940 /* TournamentCallView.swift */, + FF1162802BCF945C000C4809 /* TournamentCashierView.swift */, FF8F26522BAE0E4E00650388 /* Components */, ); path = Screen; @@ -1325,6 +1337,7 @@ C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */, FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */, FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */, + FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */, FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */, FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */, FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */, @@ -1351,8 +1364,10 @@ FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */, C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, + FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */, C4A47D742B72881F00ADC637 /* ClubView.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, + FF11627F2BCF9432000C4809 /* PlayerListView.swift in Sources */, FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, FF967CF32BAECC0B00A9A3BD /* PlayerRegistration.swift in Sources */, FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */, @@ -1385,6 +1400,7 @@ FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */, FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */, FF9268032BCE94A30080F940 /* GroupStageCallingView.swift in Sources */, + FF11627D2BCF941A000C4809 /* CashierSettingsView.swift in Sources */, FFFCDE0E2BCC833600317DEF /* LoserRoundScheduleEditorView.swift in Sources */, C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */, FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */, diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift index 7bbed12..32a2010 100644 --- a/PadelClub/Data/Federal/FederalPlayer.swift +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -21,6 +21,7 @@ protocol PlayerHolder { var clubName: String? { get } var ligueName: String? { get } var assimilation: String? { get } + var computedAge: Int? { get } } extension PlayerHolder { @@ -31,6 +32,7 @@ extension PlayerHolder { extension ImportedPlayer: PlayerHolder { + var computedAge: Int? { nil } var tournamentPlayed: Int? { Int(tournamentCount) diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index f5ffa14..26e2224 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -87,6 +87,26 @@ class PlayerRegistration: ModelObject, Storable { } } + var computedAge: Int? { + if let birthdate { + let components = birthdate.components(separatedBy: "/") + if components.count == 3 { + if let year = Calendar.current.dateComponents([.year], from: Date()).year, let age = components.last, let ageInt = Int(age) { + if age.count == 2 { //si l'année est sur 2 chiffres dans le fichier + if ageInt < 23 { + return year - 2000 - ageInt + } else { + return year - 2000 + 100 - ageInt + } + } else { //si l'année est représenté sur 4 chiffres + return year - ageInt + } + } + } + } + return nil + } + func pasteData() -> String { [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: " ") } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index c1859cf..50682bd 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -320,9 +320,19 @@ class Tournament : ModelObject, Storable { return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first } - func getActiveRound() -> Round? { + func getActiveRound(withSeeds: Bool = false) -> Round? { let rounds = rounds() - return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first + let round = rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first + + if withSeeds { + if round?.seeds().isEmpty == false { + return round + } else { + return nil + } + } else { + return round + } } func allMatches() -> [Match] { diff --git a/PadelClub/Views/Cashier/CashierDetailView.swift b/PadelClub/Views/Cashier/CashierDetailView.swift index 59cf60d..8942073 100644 --- a/PadelClub/Views/Cashier/CashierDetailView.swift +++ b/PadelClub/Views/Cashier/CashierDetailView.swift @@ -10,18 +10,38 @@ import SwiftUI struct CashierDetailView: View { var tournaments : [Tournament] + init(tournaments: [Tournament]) { + self.tournaments = tournaments + } + + init(tournament: Tournament) { + self.tournaments = [tournament] + } + var body: some View { List { ForEach(tournaments) { tournament in - _tournamentCashierDetailView(tournament) + Section { + LabeledContent { + Text(tournament.earnings().formatted(.currency(code: "EUR").precision(.fractionLength(0)))) + } label: { + Text("Encaissement") + Text(tournament.paidCompletion().formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) + } + _tournamentCashierDetailView(tournament) + } header: { + if tournaments.count > 1 { + Text(tournament.tournamentTitle()) + } + } } } .headerProminence(.increased) - .navigationTitle("Résumé") + .navigationTitle("Bilan") } private func _tournamentCashierDetailView(_ tournament: Tournament) -> some View { - Section { + DisclosureGroup { ForEach(PlayerRegistration.PaymentType.allCases) { type in let count = tournament.selectedPlayers().filter({ $0.registrationType == type }).count LabeledContent { @@ -34,8 +54,23 @@ struct CashierDetailView: View { Text(count.formatted()) } } - } header: { - Text(tournament.tournamentTitle()) + } label: { + Text("Voir le détail") } + +// +// Section { +// ForEach(tournaments) { tournament in +// } +//// HStack { +//// Text("Total") +//// Spacer() +//// Text(event.earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0)))) +//// Text(event.paidCompletion.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) +//// } +// } header: { +// Text("Encaissement") +// } + } } diff --git a/PadelClub/Views/Cashier/CashierSettingsView.swift b/PadelClub/Views/Cashier/CashierSettingsView.swift new file mode 100644 index 0000000..08d8331 --- /dev/null +++ b/PadelClub/Views/Cashier/CashierSettingsView.swift @@ -0,0 +1,56 @@ +// +// CashierSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct CashierSettingsView: View { + @EnvironmentObject var dataStore: DataStore + var tournaments: [Tournament] + + init(tournaments: [Tournament]) { + self.tournaments = tournaments + } + + init(tournament: Tournament) { + self.tournaments = [tournament] + } + + var body: some View { + List { + Section { + RowButtonView("Tout le monde a réglé", role: .destructive) { + let players = tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + if player.hasPaid() == false { + player.registrationType = .gift + } + } + try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players) + } + } footer: { + Text("Passe tous les joueurs qui n'ont pas réglé en offert") + } + + Section { + RowButtonView("Personne n'a réglé", role: .destructive) { + let players = tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + player.registrationType = nil + } + try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players) + } + } footer: { + Text("Remet à zéro le type d'encaissement de tous les joueurs") + } + + } + } +} + +#Preview { + CashierSettingsView(tournaments: []) +} diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift index bec5b24..2660f02 100644 --- a/PadelClub/Views/Cashier/CashierView.swift +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -11,60 +11,55 @@ import Combine struct CashierView: View { @EnvironmentObject var dataStore: DataStore var tournaments : [Tournament] - @Environment(\.dismiss) private var dismiss - @State var licenseCheck: Bool? + var teams: [TeamRegistration] @State private var sortOption: SortOption = .callDate @State private var filterOption: FilterOption = .all @State private var sortOrder: SortOrder = .ascending @State private var searchText = "" - @State private var licenseToEdit = "" - @State private var editPlayer: PlayerRegistration? + @State private var isSearching: Bool = false - let licenseMode: Bool - - init(event: Event, licenseCheck: Bool? = nil, sortOption: SortOption = .callDate) { - _licenseCheck = State(wrappedValue: licenseCheck) - _sortOption = State(wrappedValue: sortOption) - licenseMode = licenseCheck != nil + init(event: Event) { self.tournaments = event.tournaments + self.teams = [] } - init(tournament: Tournament, licenseCheck: Bool? = nil, sortOption: SortOption = .callDate) { - _licenseCheck = State(wrappedValue: licenseCheck) - _sortOption = State(wrappedValue: sortOption) - licenseMode = licenseCheck != nil + init(tournament: Tournament, teams: [TeamRegistration]) { self.tournaments = [tournament] + self.teams = teams } - - func somePlayerToEdit() -> Binding { - Binding { - editPlayer != nil - } set: { _ in - } + private func _sharedData() -> String { + let players = teams + .flatMap({ $0.players() }) + .map { + [$0.pasteData()] + .compacted() + .joined(separator: "\n") + } + .joined(separator: "\n\n") + return players } enum SortOption: Int, Identifiable, CaseIterable { - case round - case team + case teamRank case alphabeticalLastName case alphabeticalFirstName - case rank + case playerRank case age case callDate var id: Int { self.rawValue } func localizedLabel() -> String { switch self { - case .round, .callDate: + case .callDate: return "Convocation" - case .team: - return "Équipe" + case .teamRank: + return "Poids d'équipe" case .alphabeticalLastName: return "Nom" case .alphabeticalFirstName: return "Prénom" - case .rank: + case .playerRank: return "Rang" case .age: return "Âge" @@ -103,190 +98,9 @@ struct CashierView: View { } } - var orderedPlayers: [PlayerRegistration] { - if sortOrder == .ascending { - return sortedPlayers - } else { - return sortedPlayers.reversed() - } - } - - var orderedTeams: [TeamRegistration] { - if sortOrder == .ascending { - return sortedTeams - } else { - return sortedTeams.reversed() - } - } - - var sortedTeams: [TeamRegistration] { - tournaments.flatMap({ $0.selectedSortedTeams() }) - } - - func playerHasValidLicense(_ player: PlayerRegistration) -> Bool { - return true - //todo -// if player.isImported() { -// if let licenseCheck, let licenseYearValidity = event.licenseYearValidity { -// return player.isValidLicenseNumber(year: licenseYearValidity) == licenseCheck -// } else { -// return true -// } -// } else { -// return true -// } - } - - var searchedPlayers: [PlayerRegistration] { - tournaments.flatMap({ $0.unsortedPlayers() }) -// if searchText.trimmed.isEmpty { -// return event.orderedPlayers.filter { playerHasValidLicense($0) } -// } else { -// let search = searchText.trimmed.folding(options: .diacriticInsensitive, locale: .current).lowercased() -// return event.orderedPlayers.filter { $0.canonicalName.contains(search) && playerHasValidLicense($0) } -// } - } - - var filteredPlayers: [PlayerRegistration] { - if licenseMode == false { - return searchedPlayers.filter { filterOption.shouldDisplayPlayer($0) } - } else { - return searchedPlayers - } - } - - var sortedPlayers: [PlayerRegistration] { - switch sortOption { - case .callDate, .team, .round: - return filteredPlayers.sorted(using: .keyPath(\.lastName), .keyPath(\.firstName)) - case .alphabeticalFirstName: - return filteredPlayers.sorted(using: .keyPath(\.firstName), .keyPath(\.lastName)) - case .alphabeticalLastName: - return filteredPlayers.sorted(using: .keyPath(\.lastName), .keyPath(\.firstName)) - case .rank: - return filteredPlayers.sorted(using: .keyPath(\.weight), .keyPath(\.lastName), .keyPath(\.firstName)) - case .age: - return filteredPlayers.sorted(using: .keyPath(\.weight), .keyPath(\.lastName), .keyPath(\.firstName)) - } - } - - func save() { -// do { -// event.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)") -// } - } - - func bracketPlayers(in tournament: Tournament) -> [PlayerRegistration] { - tournament.selectedSortedTeams().filter { $0.groupStagePosition != nil }.flatMap { $0.unsortedPlayers() }.filter { orderedPlayers.contains($0) } - } - - func playersForRound(in round: Int, tournament: Tournament) -> [PlayerRegistration] { - tournament.selectedSortedTeams().filter { RoundRule.roundIndex(fromMatchIndex: 0) == round && $0.groupStagePosition == nil }.flatMap { $0.unsortedPlayers() }.filter { orderedPlayers.contains($0) } - } - - var displayOptionView: some View { - DisclosureGroup { - HStack { - Text("Voir") - Spacer() - Picker(selection: $licenseCheck) { - Text("Tous les joueurs").tag(nil as Bool?) - Text("Avec licence valide").tag(true as Bool?) - Text("Sans licence valide").tag(false as Bool?) - } label: { - } - } - HStack { - Text("Filtre") - Spacer() - Picker(selection: $filterOption) { - ForEach(FilterOption.allCases) { filterOption in - Text(filterOption.localizedLabel()).tag(filterOption) - } - } label: { - } - } - - HStack { - Text("Tri") - Spacer() - Picker(selection: $sortOption) { - ForEach(SortOption.allCases) { sortOption in - Text(sortOption.localizedLabel()).tag(sortOption) - } - } label: { - } - } - - HStack { - Text("Ordre") - Spacer() - Picker(selection: $sortOrder) { - Text("Croissant").tag(SortOrder.ascending) - Text("Décroissant").tag(SortOrder.descending) - } label: { - - } - } - } label: { - Text("Options d'affichage") - } - - } - - @ViewBuilder - var sortedPlayersView: some View { - if orderedPlayers.isEmpty { - ContentUnavailableView.search(text: searchText) - } else { - Section { - ForEach(orderedPlayers) { player in - computedPlayerView(player) - } - } header: { - HStack { - Text(orderedPlayers.count.formatted() + " joueurs") - } - } - } - } - var body: some View { List { - Section { - NavigationLink { - CashierDetailView(tournaments: tournaments) - } label: { - Text("Résumé") - } - } - - Section { - ForEach(tournaments) { tournament in - HStack { - Text(tournament.tournamentTitle()) - Spacer() - Text(tournament.earnings().formatted(.currency(code: "EUR").precision(.fractionLength(0)))) - Text(tournament.paidCompletion().formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) - } - } -// HStack { -// Text("Total") -// Spacer() -// Text(event.earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0)))) -// Text(event.paidCompletion.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) -// } - } header: { - Text("Encaissement") - } - - if licenseMode == false { + if isSearching == false { Section { Picker(selection: $filterOption) { ForEach(FilterOption.allCases) { filterOption in @@ -304,236 +118,194 @@ struct CashierView: View { Text("Affichage par") } - if sortOption != .round { - Picker(selection: $sortOrder) { - Text("Croissant").tag(SortOrder.ascending) - Text("Décroissant").tag(SortOrder.descending) - } label: { - Text(sortOption == .team ? "Tri par rang" : "Tri") - } + Picker(selection: $sortOrder) { + Text("Croissant").tag(SortOrder.ascending) + Text("Décroissant").tag(SortOrder.descending) + } label: { + Text("Trier par ordre") } } header: { Text("Options d'affichage") } } - if searchText.isEmpty == false { - sortedPlayersView - } else { - - if sortOption == .team && licenseMode == false { - if orderedTeams.isEmpty { - ContentUnavailableView.search(text: searchText) - } else { - ForEach(orderedTeams) { team in - Section { - ForEach(team.players()) { player in - computedPlayerView(player) - } - } - } - } - } else if sortOption == .round && licenseMode == false { - if orderedPlayers.isEmpty { - ContentUnavailableView.search(text: searchText) - } else { - ForEach(tournaments) { tournament in - let bracketPlayers = bracketPlayers(in: tournament) - Section { - DisclosureGroup { - ForEach(bracketPlayers) { player in - computedPlayerView(player) - } - } label: { - HStack { - Text("Poules") - Spacer() - Text(bracketPlayers.count.formatted()) - } - } - } header: { - Text(tournament.tournamentTitle()) - } - } - - // ForEach(1...event.rounds) { round in - // ForEach(tournaments) { tournament in - // if tournament.isRoundHidden(round) == false { - // let players = playersForRound(in: round, tournament: tournament) - // - // if players.isEmpty == false { - // Section { - // DisclosureGroup { - // ForEach(players) { player in - // computedPlayerView(player) - // } - // } label: { - // HStack { - // Text(RoundLabel.labels[tournament.rounds - round]) - // Spacer() - // Text(players.count.formatted()) - // } - // } - // } header: { - // Text(tournament.localizedTitle) - // } - // } - // } - // } - // } - } - } else if sortOption == .callDate && licenseMode == false { - _byCallDateView() - } else { - sortedPlayersView - } + if _isContentUnavailable() { + _contentUnavailableView() + } + + switch sortOption { + case .teamRank: + _byTeamRankView() + case .alphabeticalLastName: + _byPlayerLastName() + case .alphabeticalFirstName: + _byPlayerFirstName() + case .playerRank: + _byPlayerRank() + case .age: + _byPlayerAge() + case .callDate: + _byCallDateView() } } -// .alert("Licence", isPresented: somePlayerToEdit(), actions: { -// TextField("Licence", text: $licenseToEdit) -// .keyboardType(.asciiCapable) -// .autocorrectionDisabled(true) -// .textContentType(.init(rawValue: "")) -// Button("OK") { -// editPlayer?.license = licenseToEdit -// licenseToEdit = "" -// editPlayer?.objectWillChange.send() -// editPlayer = nil -// save() -// } -// Button("Annuler", role : .cancel) { -// licenseToEdit = "" -// editPlayer = nil -// } -// }) + .headerProminence(.increased) + .searchable(text: $searchText, isPresented: $isSearching, prompt: Text("Chercher un joueur")) .toolbar { ToolbarItem(placement: .topBarTrailing) { - Menu { -// Button { -// event.orderedPlayers.forEach { player in -// if let registration = player.orderedRegistrations.first(where: { $0.tournament?.tournamentID == player.tournament?.tournamentID }) { -// if registration.paymentType == .notPaid { -// registration.paymentType = .gift -// } -// } else { -// let registration = Registration(context: viewContext) -// registration.paymentType = .gift -// registration.tournament = player.tournament -// player.addToRegistrations(registration) -// } -// save() -// } -// } label: { -// Text("Tout le monde a réglé") -// } -// -// Button { -// event.orderedPlayers.forEach { player in -// if let registration = player.orderedRegistrations.first(where: { $0.tournament?.tournamentID == player.tournament?.tournamentID }) { -// registration.paymentType = .notPaid -// } else { -// let registration = Registration(context: viewContext) -// registration.paymentType = .notPaid -// registration.tournament = player.tournament -// player.addToRegistrations(registration) -// } -// save() -// } -// } label: { -// Text("Personne n'a réglé") -// } -// - } label: { - LabelOptions() - } + ShareLink(item: _sharedData()) } } - .navigationTitle("Joueurs") - .navigationBarTitleDisplayMode(.large) - .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher un joueur")) } @ViewBuilder func computedPlayerView(_ player: PlayerRegistration) -> some View { - VStack(alignment: .leading) { - ImportedPlayerView(player: player) - HStack { - Menu { - if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") { - Link(destination: url) { - Label("Appeler", systemImage: "phone") - } - } - if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "sms:\(number)") { - Link(destination: url) { - Label("SMS", systemImage: "message") - } - } - - Divider() - if let licenseCheck, let licenseYearValidity = player.tournament()?.licenseYearValidity(), licenseCheck == false { - Button { - player.validateLicenceId(licenseYearValidity) - save() - - if filteredPlayers.isEmpty { - dismiss() - } - - } label: { - Text("Valider la licence \(licenseYearValidity)") - } + EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) + } + + private func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool { + team.players().allSatisfy({ + _shouldDisplayPlayer($0) + }) + } + + private func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { + if searchText.isEmpty == false { + filterOption.shouldDisplayPlayer(player) && player.contains(searchText) + } else { + filterOption.shouldDisplayPlayer(player) + } + } + + @ViewBuilder + private func _byPlayer(_ players: [PlayerRegistration]) -> some View { + let _players = sortOrder == .ascending ? players : players.reversed() + ForEach(_players) { player in + Section { + computedPlayerView(player) + } header: { + HStack { + if let teamCallDate = player.team()?.callDate { + Text(teamCallDate.localizedDate()) } - - if let license = player.licenceId?.strippedLicense { - Button { - let pasteboard = UIPasteboard.general - pasteboard.string = license - } label: { - Label("Copier la licence", systemImage: "doc.on.doc") + Spacer() + Text(player.weight.formatted()) + } + } footer: { + if tournaments.count > 1, let tournamentTitle = player.tournament()?.tournamentTitle() { + Text(tournamentTitle) + } + } + } + } + + @ViewBuilder + private func _byPlayerRank() -> some View { + let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.weight)).filter({ _shouldDisplayPlayer($0) }) + _byPlayer(players) + } + + @ViewBuilder + private func _byPlayerAge() -> some View { + let players = teams.flatMap({ $0.players() }).filter({ $0.computedAge != nil }).sorted(using: .keyPath(\.computedAge!)).filter({ _shouldDisplayPlayer($0) }) + _byPlayer(players) + } + + @ViewBuilder + private func _byPlayerLastName() -> some View { + let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.lastName)).filter({ _shouldDisplayPlayer($0) }) + _byPlayer(players) + } + + @ViewBuilder + private func _byPlayerFirstName() -> some View { + let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.firstName)).filter({ _shouldDisplayPlayer($0) }) + _byPlayer(players) + } + + @ViewBuilder + private func _byTeamRankView() -> some View { + let _teams = sortOrder == .ascending ? teams : teams.reversed() + ForEach(_teams) { team in + if _shouldDisplayTeam(team) { + Section { + _cashierPlayersView(team.players()) + } header: { + HStack { + if let callDate = team.callDate { + Text(callDate.localizedDate()) } + Spacer() + Text(team.weight.formatted()) } - - Section { - Button { - licenseToEdit = player.licenceId ?? "" - editPlayer = player - } label: { - if player.licenceId == nil { - Text("Ajouter la licence") - } else { - Text("Modifier la licence") - } - } - PasteButton(payloadType: String.self) { strings in - guard let first = strings.first else { return } - player.licenceId = first - } - } header: { - Text("Modification de licence") + } footer: { + if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { + Text(tournamentTitle) } - } label: { - Text("Options") } - Spacer() - PlayerPayView(player: player) } } } + @ViewBuilder private func _byCallDateView() -> some View { - ForEach(tournaments) { tournament in - let teams = tournament.selectedSortedTeams() - let players = teams.filter({ $0.callDate != nil }).sorted(using: .keyPath(\.callDate!)).flatMap({ $0.players() }) + teams.filter({ $0.callDate == nil }).flatMap({ $0.players() }) - Section { - ForEach(players) { player in - computedPlayerView(player) + let groupedTeams = Dictionary(grouping: teams) { team in + team.callDate + } + let keys = sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed() + + ForEach(keys, id: \.self) { key in + if let _teams = groupedTeams[key] { + ForEach(_teams) { team in + if _shouldDisplayTeam(team) { + Section { + _cashierPlayersView(team.players()) + } header: { + Text(key.localizedDate()) + } footer: { + if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { + Text(tournamentTitle) + } + } + } } - } header: { - Text(tournament.tournamentTitle()) } - .headerProminence(.increased) + } + } + + @ViewBuilder + private func _cashierPlayersView(_ players: [PlayerRegistration]) -> some View { + ForEach(players) { player in + if _shouldDisplayPlayer(player) { + computedPlayerView(player) + } + } + } + + private func _isContentUnavailable() -> Bool { + switch sortOption { + case .teamRank, .callDate: + return teams.filter({ _shouldDisplayTeam($0) }).isEmpty + default: + return teams.flatMap({ $0.players() }).filter({ _shouldDisplayPlayer($0) }).isEmpty + } + } + + private func _unavailableIcon() -> String { + switch sortOption { + case .teamRank, .callDate: + return "person.2.slash.fill" + default: + return "person.slash.fill" + } + } + + @ViewBuilder + private func _contentUnavailableView() -> some View { + if isSearching { + ContentUnavailableView.search(text: searchText) + } else { + ContentUnavailableView("Aucun résultat", systemImage: _unavailableIcon()) } } } diff --git a/PadelClub/Views/Cashier/PlayerListView.swift b/PadelClub/Views/Cashier/PlayerListView.swift new file mode 100644 index 0000000..14ddd19 --- /dev/null +++ b/PadelClub/Views/Cashier/PlayerListView.swift @@ -0,0 +1,18 @@ +// +// PlayerListView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct PlayerListView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + PlayerListView() +} diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift new file mode 100644 index 0000000..a5ad38a --- /dev/null +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -0,0 +1,100 @@ +// +// EditablePlayerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct EditablePlayerView: View { + + enum PlayerEditingOption { + case payment + case licenceId + } + + @EnvironmentObject var dataStore: DataStore + var player: PlayerRegistration + var editingOptions: [PlayerEditingOption] + @State private var editedLicenceId = "" + @State private var shouldPresentLicenceIdEdition: Bool = false + + var body: some View { + computedPlayerView(player) + .alert("Numéro de licence", isPresented: $shouldPresentLicenceIdEdition) { + TextField("Numéro de licence", text: $editedLicenceId) + .onSubmit { + player.licenceId = editedLicenceId + editedLicenceId = "" + try? dataStore.playerRegistrations.addOrUpdate(instance: player) + } + } + } + + @ViewBuilder + func computedPlayerView(_ player: PlayerRegistration) -> some View { + VStack(alignment: .leading) { + ImportedPlayerView(player: player) + HStack { + Menu { + if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") { + Link(destination: url) { + Label("Appeler", systemImage: "phone") + } + } + if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "sms:\(number)") { + Link(destination: url) { + Label("SMS", systemImage: "message") + } + } + + if editingOptions.contains(.licenceId) { + Divider() + if let licenseYearValidity = player.tournament()?.licenseYearValidity(), player.isValidLicenseNumber(year: licenseYearValidity) == false, player.licenceId != nil { + Button { + player.validateLicenceId(licenseYearValidity) + } label: { + Text("Valider la licence \(licenseYearValidity)") + } + } + } + + if let license = player.licenceId?.strippedLicense { + Button { + let pasteboard = UIPasteboard.general + pasteboard.string = license + } label: { + Label("Copier la licence", systemImage: "doc.on.doc") + } + } + + Section { + Button { + editedLicenceId = player.licenceId ?? "" + shouldPresentLicenceIdEdition = true + } label: { + if player.licenceId == nil { + Text("Ajouter la licence") + } else { + Text("Modifier la licence") + } + } + PasteButton(payloadType: String.self) { strings in + guard let first = strings.first else { return } + player.licenceId = first + } + } header: { + Text("Modification de licence") + } + } label: { + Text("Options") + } + if editingOptions.contains(.payment) { + Spacer() + PlayerPayView(player: player) + } + } + } + } +} diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index c3ffb0c..e010b79 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -29,6 +29,9 @@ struct ImportedPlayerView: View { .foregroundStyle(.secondary) .font(.caption) } + } else if let computedAge = player.computedAge { + Text(computedAge.formatted() + " ans") + .foregroundStyle(.secondary) } } .font(.title3) diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift index 4f7d8e9..02c6d7d 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -134,7 +134,7 @@ struct InscriptionInfoView: View { Section { DisclosureGroup { ForEach(playersWithoutValidLicense) { - ImportedPlayerView(player: $0) + EditablePlayerView(player: $0, editingOptions: [.licenceId]) } } label: { LabeledContent { diff --git a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift new file mode 100644 index 0000000..b6a6445 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift @@ -0,0 +1,100 @@ +// +// TournamentCashierView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +enum CashierDestination: Identifiable, Selectable { + case summary + case groupStage(GroupStage) + case bracket(Round) + case all + + var id: String { + switch self { + case .summary, .all: + return String(describing: self) + case .groupStage(let groupStage): + return groupStage.id + case .bracket(let round): + return round.id + } + } + + func selectionLabel() -> String { + switch self { + case .summary: + return "Bilan" + case .groupStage(let groupStage): + return groupStage.selectionLabel() + case .bracket(let round): + return round.selectionLabel() + case .all: + return "Tous" + } + } + + func badgeValue() -> Int? { + nil + } +} + +struct TournamentCashierView: View { + var tournament: Tournament + @State private var selectedDestination: CashierDestination? + + func allDestinations() -> [CashierDestination] { + var allDestinations : [CashierDestination] = [.summary, .all] + let destinations : [CashierDestination] = tournament.groupStages().map { CashierDestination.groupStage($0) } + allDestinations.append(contentsOf: destinations) + tournament.rounds().forEach { round in + if round.seeds().isEmpty == false { + allDestinations.append(CashierDestination.bracket(round)) + } + } + return allDestinations + } + + init(tournament: Tournament) { + self.tournament = tournament + let gs = tournament.getActiveGroupStage() + if let gs { + _selectedDestination = State(wrappedValue: .groupStage(gs)) + } else if let rs = tournament.getActiveRound(withSeeds: true) { + _selectedDestination = State(wrappedValue: .bracket(rs)) + } + } + + var body: some View { + VStack(spacing: 0) { + GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) + switch selectedDestination { + case .none: + CashierSettingsView(tournament: tournament) + .navigationTitle("Réglages") + case .some(let selectedCall): + switch selectedCall { + case .summary: + CashierDetailView(tournament: tournament) + case .groupStage(let groupStage): + CashierView(tournament: tournament, teams: groupStage.teams()) + case .bracket(let round): + CashierView(tournament: tournament, teams: round.seeds()) + case .all: + CashierView(tournament: tournament, teams: tournament.selectedSortedTeams()) + } + } + } + .environment(tournament) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Encaissement") + } +} + +#Preview { + TournamentCashierView(tournament: Tournament.mock()) +} diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 0c07383..c27cfde 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -81,7 +81,7 @@ struct TournamentView: View { case .schedule: TournamentScheduleView(tournament: tournament) case .cashier: - CashierView(tournament: tournament) + TournamentCashierView(tournament: tournament) case .call: TournamentCallView(tournament: tournament) }