From d131eae629f938ad3e6475070da11325702ba3af Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 27 Mar 2024 16:24:24 +0100 Subject: [PATCH] clean up --- PadelClub.xcodeproj/project.pbxproj | 12 ++ PadelClub/Data/MockData.swift | 7 + PadelClub/Data/PlayerRegistration.swift | 11 + PadelClub/Data/TeamRegistration.swift | 16 +- PadelClub/Data/Tournament.swift | 97 +++++---- PadelClub/Extensions/Color+Extensions.swift | 33 +++ PadelClub/Extensions/Date+Extensions.swift | 30 +++ PadelClub/Manager/Tips.swift | 36 ++++ PadelClub/PadelClubApp.swift | 3 + PadelClub/Views/Event/EventCreationView.swift | 57 ++++-- .../Components/PlayerSexPickerView.swift | 17 +- .../Views/Tournament/FileImportView.swift | 102 +++++----- .../Components/InscriptionInfoView.swift | 191 ++++++++++++++++++ .../Screen/InscriptionManagerView.swift | 62 +++--- .../Shared/TournamentCellView.swift | 2 +- .../Views/Tournament/TournamentView.swift | 47 +++-- .../ViewModifiers/ListRowViewModifier.swift | 33 +++ 17 files changed, 598 insertions(+), 158 deletions(-) create mode 100644 PadelClub/Extensions/Color+Extensions.swift create mode 100644 PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift create mode 100644 PadelClub/Views/ViewModifiers/ListRowViewModifier.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 6186e26..5c5b051 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -101,6 +101,9 @@ FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; }; FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; }; FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */; }; + FF5D0D742BB41DF8005CB568 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D732BB41DF8005CB568 /* Color+Extensions.swift */; }; + FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D752BB428B2005CB568 /* ListRowViewModifier.swift */; }; + FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */; }; FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; }; @@ -311,6 +314,9 @@ FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = ""; }; FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreSheetView.swift; sourceTree = ""; }; + FF5D0D732BB41DF8005CB568 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = ""; }; + FF5D0D752BB428B2005CB568 /* ListRowViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViewModifier.swift; sourceTree = ""; }; + FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionInfoView.swift; sourceTree = ""; }; FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = ""; }; FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = ""; }; FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; @@ -796,6 +802,7 @@ FF8F26482BAE0B4100650388 /* TournamentFormatSelectionView.swift */, FF8F26492BAE0B4100650388 /* TournamentLevelPickerView.swift */, FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */, + FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */, ); path = Components; sourceTree = ""; @@ -848,6 +855,7 @@ isa = PBXGroup; children = ( FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */, + FF5D0D752BB428B2005CB568 /* ListRowViewModifier.swift */, ); path = ViewModifiers; sourceTree = ""; @@ -877,6 +885,7 @@ FF6EC9032B9479F500EA7F5A /* Sequence+Extensions.swift */, FF6EC9082B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift */, FF6EC90A2B947AC000EA7F5A /* Array+Extensions.swift */, + FF5D0D732BB41DF8005CB568 /* Color+Extensions.swift */, FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */, ); path = Extensions; @@ -1089,11 +1098,13 @@ FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */, FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */, FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */, + FF5D0D742BB41DF8005CB568 /* Color+Extensions.swift in Sources */, C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */, FF7091682B90F79F00AB08DA /* TournamentCellView.swift in Sources */, FF6EC9042B9479F500EA7F5A /* Sequence+Extensions.swift in Sources */, C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */, FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */, + FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */, FFD783FD2B91B9ED000F62A6 /* AgendaDestinationPickerView.swift in Sources */, FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */, FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */, @@ -1179,6 +1190,7 @@ FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */, FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */, C4A47D922B7BBBEC00ADC637 /* StoreItem.swift in Sources */, + FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */, FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */, FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */, C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */, diff --git a/PadelClub/Data/MockData.swift b/PadelClub/Data/MockData.swift index 9ee06bb..ada3ae4 100644 --- a/PadelClub/Data/MockData.swift +++ b/PadelClub/Data/MockData.swift @@ -36,6 +36,13 @@ extension Tournament { let femaleUnrankedValue : Int? = lastDataSourceFemaleUnranked == 0 ? nil : lastDataSourceMaleUnranked let rankSourceDate = _mostRecentDateAvailable + //todo + /* + tournament.tournamentLevel = TournamentLevel.mostUsed(tournaments: tournaments) + tournament.tournamentCategory = TournamentCategory.mostUsed(tournaments: tournaments) + tournament.federalTournamentAge = FederalTournamentAge.mostUsed(tournaments: tournaments) + */ + return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior, maleUnrankedValue: maleUnrankedValue, femaleUnrankedValue: femaleUnrankedValue) } } diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 74e3caf..01fc8f6 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -130,6 +130,17 @@ class PlayerRegistration: ModelObject, Storable { } } + func isImported() -> Bool { + source == .beachPadel + } + + func isValidLicenseNumber(year: Int) -> Bool { + guard let licenceId else { return false } + guard licenceId.isLicenseNumber else { return false } + guard licenceId.suffix(6) == "(\(year))" else { return false } + return true + } + @objc var canonicalName: String { playerLabel().folding(options: .diacriticInsensitive, locale: .current).lowercased() diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 99f380f..04fcc1e 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -31,6 +31,7 @@ class TeamRegistration: ModelObject, Storable { var category: Int? var weight: Int = 0 var lockWeight: Int? + var confirmationDate: Date? internal init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, category: Int? = nil) { self.tournament = tournament @@ -51,8 +52,16 @@ class TeamRegistration: ModelObject, Storable { lockWeight ?? weight } + func called() -> Bool { + callDate != nil + } + + func confirmed() -> Bool { + confirmationDate != nil + } + func isImported() -> Bool { - unsortedPlayers().allSatisfy({ $0.source == .beachPadel }) + unsortedPlayers().allSatisfy({ $0.isImported() }) } func isWildCard() -> Bool { @@ -79,6 +88,10 @@ class TeamRegistration: ModelObject, Storable { $0.clubName?.contains(codeClubOrClubName) == true || $0.clubName?.contains(codeClubOrClubName) == true }) } + + func updateWeight() { + setWeight(from: self.players()) + } func teamLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { @@ -248,6 +261,7 @@ class TeamRegistration: ModelObject, Storable { case _weight = "weight" case _walkOut = "walkOut" case _lockWeight = "lockWeight" + case _confirmationDate = "confirmationDate" } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index b18e6e6..555d88b 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -89,6 +89,10 @@ class Tournament : ModelObject, Storable { case build } + func hasStarted() -> Bool { + startDate <= Date() + } + var eventObject: Event? { guard let event else { return nil } return Store.main.findById(event) @@ -122,10 +126,6 @@ class Tournament : ModelObject, Storable { Store.main.filter { $0.tournament == self.id }.sorted(by: \.index) } - var clubName: String? { - nil - } - func sortedTeams() -> [TeamRegistration] { let teams = selectedSortedTeams() return teams + waitingListTeams(in: teams) @@ -169,7 +169,8 @@ class Tournament : ModelObject, Storable { } func waitingListTeams(in teams: [TeamRegistration]) -> [TeamRegistration] { - Set(unsortedTeams()).subtracting(teams).sorted(using: _defaultSorting(), order: .ascending) + let waitingList = Set(unsortedTeams()).subtracting(teams) + return waitingList.filter { $0.walkOut == false }.sorted(using: _defaultSorting(), order: .ascending) + waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending) } func bracketCut() -> Int { @@ -193,32 +194,9 @@ class Tournament : ModelObject, Storable { func unsortedTeams() -> [TeamRegistration] { Store.main.filter { $0.tournament == self.id } } - - typealias TeamRegistrationCompare = (TeamRegistration, TeamRegistration) -> Bool - - func teams() -> [TeamRegistration] { - Store.main.filter { $0.tournament == self.id } - .sorted { (lhs, rhs) in - let predicates: [TeamRegistrationCompare] = [ - { $0.weight < $1.weight }, - { $0.registrationDate ?? .distantPast < $1.registrationDate ?? .distantPast }, - ] - - for predicate in predicates { - if !predicate(lhs, rhs) && !predicate(rhs, lhs) { - continue - } - - return predicate(lhs, rhs) - } - - return false - } - } - func duplicates() -> [PlayerRegistration] { + func duplicates(in players: [PlayerRegistration]) -> [PlayerRegistration] { var duplicates = [PlayerRegistration]() - let players = unsortedPlayers() Set(players.compactMap({ $0.licenceId })).forEach { licenceId in let found = players.filter({ $0.licenceId == licenceId }) if found.count > 1 { @@ -250,15 +228,62 @@ class Tournament : ModelObject, Storable { return malePlayer ? maleUnrankedValue : femaleUnrankedValue } } - + + //todo + var clubName: String? { + nil + } + + //todo var rounds: Int { 4 } + //todo func significantPlayerCount() -> Int { 2 } + func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] { + if startDate.isInCurrentYear() == false { + return [] + } + return players.filter { player in + if player.rank == nil { return false } + if player.weight <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge) { + return true + } else { + return false + } + } + } + + func mandatoryRegistrationCloseDate() -> Date? { + switch tournamentLevel { + case .p500, .p1000, .p1500, .p2000: + if let date = Calendar.current.date(byAdding: .day, value: -6, to: startDate) { + let startOfDay = Calendar.current.startOfDay(for: date) + return Calendar.current.date(byAdding: .minute, value: -1, to: startOfDay) + } + default: + break + } + return nil + } + + func licenseYearValidity() -> Int { + if startDate.get(.month) > 8 { + return startDate.get(.year) + 1 + } else { + return startDate.get(.year) + } + } + + func playersWithoutValidLicense(in players: [PlayerRegistration]) -> [PlayerRegistration] { + let licenseYearValidity = licenseYearValidity() + return players.filter({ ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.licenceId?.isLicenseNumber == false || $0.licenceId?.isEmpty == true)) }) + } + func importTeams(_ teams: [FileImportManager.TeamHolder]) { var teamsToImport = [TeamRegistration]() teams.forEach { team in @@ -278,6 +303,10 @@ class Tournament : ModelObject, Storable { func lockRegistration() { closedRegistrationDate = Date() + let count = selectedSortedTeams().count + if teamCount != count { + teamCount = count + } let teams = unsortedTeams() teams.forEach { team in team.lockWeight = team.weight @@ -314,12 +343,6 @@ class Tournament : ModelObject, Storable { await MainActor.run { self.maleUnrankedValue = lastRankMan self.femaleUnrankedValue = lastRankWoman - -// if inscriptionClosed == false { -// orderedEntries.forEach { entrant in -// entrant.weightAtRegistration = entrant.updatedRank -// } -// } } } @@ -460,7 +483,7 @@ class Tournament : ModelObject, Storable { return } let max = groupStages.map { $0.size }.reduce(0,+) - var chunks = teams().filter { $0.wildCardBracket == false }.suffix(max).chunked(into: numberOfBracketsAsInt) + var chunks = selectedSortedTeams().suffix(max).chunked(into: numberOfBracketsAsInt) for (index, _) in chunks.enumerated() { if randomize { chunks[index].shuffle() diff --git a/PadelClub/Extensions/Color+Extensions.swift b/PadelClub/Extensions/Color+Extensions.swift new file mode 100644 index 0000000..eaa8d41 --- /dev/null +++ b/PadelClub/Extensions/Color+Extensions.swift @@ -0,0 +1,33 @@ +// +// Color+Extensions.swift +// PadelClub +// +// Created by Razmig Sarkissian on 27/03/2024. +// + +import SwiftUI + +extension Color { + func variation(withHueOffset hueOffset: Double = 0, saturationFactor: Double = 0.4, brightnessFactor: Double = 0.8, opacity: Double = 0.5) -> Color { + var hue: CGFloat = 0 + var saturation: CGFloat = 0 + var brightness: CGFloat = 0 + var alpha: CGFloat = 0 + + UIColor(self).getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + + // Apply adjustments + hue += CGFloat(hueOffset) + saturation *= CGFloat(saturationFactor) + brightness *= CGFloat(brightnessFactor) + alpha *= CGFloat(opacity) + + // Clamp values + hue = max(0, min(hue, 1)) + saturation = max(0, min(saturation, 1)) + brightness = max(0, min(brightness, 1)) + alpha = max(0, min(alpha, 1)) + + return Color(hue: Double(hue), saturation: Double(saturation), brightness: Double(brightness), opacity: Double(alpha)) + } +} diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index b7ec13c..59d96b8 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -12,3 +12,33 @@ extension Date { formatted(.dateTime.month(.wide).year(.defaultDigits)) } } + +extension Date { + func isInCurrentYear() -> Bool { + let calendar = Calendar.current + let currentYear = calendar.component(.year, from: Date()) + let yearOfDate = calendar.component(.year, from: self) + + return currentYear == yearOfDate + } + + func get(_ components: Calendar.Component..., calendar: Calendar = Calendar.current) -> DateComponents { + return calendar.dateComponents(Set(components), from: self) + } + + func get(_ component: Calendar.Component, calendar: Calendar = Calendar.current) -> Int { + return calendar.component(component, from: self) + } + + var tomorrowAtNine: Date { + let currentHour = Calendar.current.component(.hour, from: self) + let startOfDay = Calendar.current.startOfDay(for: self) + if currentHour < 8 { + return Calendar.current.date(byAdding: .hour, value: 9, to: startOfDay)! + } else { + let date = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay) + return Calendar.current.date(byAdding: .hour, value: 9, to: date!)! + } + } +} + diff --git a/PadelClub/Manager/Tips.swift b/PadelClub/Manager/Tips.swift index ccfe85e..c5d9a1a 100644 --- a/PadelClub/Manager/Tips.swift +++ b/PadelClub/Manager/Tips.swift @@ -298,6 +298,42 @@ struct SlideToDeleteTip: Tip { } +struct MultiTournamentsEventTip: Tip { + var title: Text { + Text("Plusieurs tournois le même week-end ?") + } + + var message: Text? { + Text("Padel Club permet de gérer plusieurs tournois ayant lieu en même temps. Un P100 homme et dame par le même week-end par exemple.") + } + + var image: Image? { + Image(systemName: "trophy.circle") + } + + var actions: [Action] { + Action(id: ActionKey.addEvent.rawValue, title: "Ajoutez une épreuve") + } + + enum ActionKey: String { + case addEvent = "add-event" + } +} + +struct NotFoundAreWalkOutTip: Tip { + var title: Text { + Text("Gestion des équipes manquantes") + } + + var message: Text? { + Text("Si une équipe déjà présente dans votre liste d'attente n'est pas dans le fichier, elle sera mise WO") + } + + var image: Image? { + Image(systemName: "person.2.slash.fill") + } +} + struct TipStyleModifier: ViewModifier { @Environment(\.colorScheme) var colorScheme var tint: Color? diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 5f7d9c2..7f46fed 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -20,6 +20,9 @@ struct PadelClubApp: App { self._onAppear() } .task { + + //try? Tips.resetDatastore() + try? Tips.configure([ .displayFrequency(.immediate), .datastoreLocation(.applicationDefault) diff --git a/PadelClub/Views/Event/EventCreationView.swift b/PadelClub/Views/Event/EventCreationView.swift index 50478fb..d6c2e2b 100644 --- a/PadelClub/Views/Event/EventCreationView.swift +++ b/PadelClub/Views/Event/EventCreationView.swift @@ -6,16 +6,18 @@ // import SwiftUI +import TipKit struct EventCreationView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject var dataStore: DataStore @State private var eventType: EventType = .approvedTournament @State private var animationType: AnimationType = .upAndDown - @State private var startingDate: Date = Date() + @State private var startingDate: Date = Date().tomorrowAtNine @State private var duration: Int = 3 @State private var eventName: String = "" @State var tournaments: [Tournament] = [] + let multiTournamentsEventTip = MultiTournamentsEventTip() var body: some View { NavigationStack { @@ -34,6 +36,14 @@ struct EventCreationView: View { TextField("Nom de l'événement", text: $eventName) } + Section { + TipView(multiTournamentsEventTip) { action in + let tournament = Tournament.newEmptyInstance() + self.tournaments.append(tournament) + } + .tipStyle(tint: .orange) + } + Section { DatePicker(selection: $startingDate) { Text(startingDate.formatted(.dateTime.weekday(.wide)).capitalized) @@ -64,15 +74,27 @@ struct EventCreationView: View { animationEditorView } } + .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Annuler", role: .cancel) { dismiss() } } - - ToolbarItem(placement: .confirmationAction) { - Button("Valider") { + + ToolbarItem(placement: .topBarTrailing) { + Button { + let tournament = Tournament.newEmptyInstance() + self.tournaments.append(tournament) + } label: { + Label("épreuve", systemImage: "plus.circle.fill").labelStyle(.titleAndIcon) + } + .clipShape(Capsule()) + .buttonStyle(.bordered) + } + + ToolbarItem(placement: .bottomBar) { + Button { if tournaments.count > 1 || eventName.trimmed.isEmpty == false { let event = Event(name: eventName) tournaments.forEach { tournament in @@ -80,11 +102,21 @@ struct EventCreationView: View { } try? dataStore.events.addOrUpdate(instance: event) } + + tournaments.forEach { tournament in + tournament.startDate = startingDate + tournament.dayDuration = duration + } + try? dataStore.tournaments.addOrUpdate(contentOfs: tournaments) dismiss() + } label: { + Text("Valider") + .frame(maxWidth: .infinity) } - .clipShape(Capsule()) - .buttonStyle(.bordered) + .font(.headline) + .buttonStyle(.borderedProminent) + .tint(.launchScreenBackground) } } @@ -97,26 +129,21 @@ struct EventCreationView: View { ForEach(tournaments) { tournament in Section { TournamentConfigurationView(tournament: tournament) - } header: { + } footer: { if tournaments.count > 1 { HStack { Spacer() - Button { + Button(role: .destructive) { tournaments.removeAll(where: { $0 == tournament }) -// viewContext.delete(tournament) } label: { - Text("effacer") + LabelDelete() } .textCase(nil) + .font(.caption) } } } } - - RowButtonView(title: "Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") { - let tournament = Tournament.newEmptyInstance() - self.tournaments.append(tournament) - } } @ViewBuilder diff --git a/PadelClub/Views/Player/Components/PlayerSexPickerView.swift b/PadelClub/Views/Player/Components/PlayerSexPickerView.swift index 31a8752..a567bb1 100644 --- a/PadelClub/Views/Player/Components/PlayerSexPickerView.swift +++ b/PadelClub/Views/Player/Components/PlayerSexPickerView.swift @@ -8,6 +8,8 @@ import SwiftUI struct PlayerSexPickerView: View { + @EnvironmentObject var dataStore: DataStore + @Environment(Tournament.self) var tournament: Tournament @Bindable var player: PlayerRegistration var body: some View { @@ -23,16 +25,20 @@ struct PlayerSexPickerView: View { .pickerStyle(.segmented) .fixedSize() .onChange(of: player.sex) { - save() + _save() } } } - func save() { + private func _save() { do { -// player.objectWillChange.send() -// player.team?.entrant?.tournament?.objectWillChange.send() -// try viewContext.save() + player.setWeight(in: tournament) + try dataStore.playerRegistrations.addOrUpdate(instance: player) + if let team = player.team() { + team.updateWeight() + try dataStore.teamRegistrations.addOrUpdate(instance: team) + } + } 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. @@ -45,4 +51,5 @@ struct PlayerSexPickerView: View { #Preview { PlayerSexPickerView(player: PlayerRegistration.mock()) + .environment(Tournament.mock()) } diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 05e9213..b404cc2 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import TipKit struct FileImportView: View { @EnvironmentObject var dataStore: DataStore @@ -13,7 +14,8 @@ struct FileImportView: View { @Environment(\.dismiss) private var dismiss let fileContent: String? - + let notFoundAreWalkOutTip = NotFoundAreWalkOutTip() + @State private var teams: [FileImportManager.TeamHolder] = [] @State private var isShowing = false @State private var didImport = false @@ -24,7 +26,6 @@ struct FileImportView: View { @State private var selectedOptions: Set = Set() @State private var fileProvider: FileImportManager.FileProvider = .frenchFederation - let federalLink = URL(string: "https://beach-padel.app.fft.fr/beachja/index/")! private var filteredTeams: [FileImportManager.TeamHolder] { return teams.filter { $0.tournamentCategory == tournament.tournamentCategory }.sorted(by: \.weight) @@ -35,8 +36,8 @@ struct FileImportView: View { if teams.isEmpty { Section { - Link(destination: federalLink) { - Label("Ouvrir beach-padel.app.fft.fr", systemImage: "tennisball") + Link(destination: SourceFileManager.beachPadel) { + Label("beach-padel.app.fft.fr", systemImage: "tennisball") } Button { @@ -48,37 +49,34 @@ struct FileImportView: View { } header: { } footer: { - VStack(alignment: .leading) { - Text("Fichier provenant de beach-padel.app.fft.fr") - Text("Format XLS ou CSV, onglet inscriptions ou joueurs") - } + Text("Fichier provenant de beach-padel.app.fft.fr") } } - if filteredTeams.isEmpty == false && tournament.unsortedTeams().isEmpty == false { - Section { - ForEach(TeamImportStrategy.allCases, id: \.self) { strategy in - LabeledContent { - Toggle(isOn: .init(get: { - selectedOptions.contains(strategy) - }, set: { selected in - if selected { - selectedOptions.insert(strategy) - } else { - selectedOptions.remove(strategy) - - } - })) {} - } label: { - Text(strategy.titleLabel()) - Text(strategy.descriptionLabel()) - } - - } - } header: { - Text("Stratégie d'importation") - } - } +// if filteredTeams.isEmpty == false && tournament.unsortedTeams().isEmpty == false { +// Section { +// ForEach(TeamImportStrategy.allCases, id: \.self) { strategy in +// LabeledContent { +// Toggle(isOn: .init(get: { +// selectedOptions.contains(strategy) +// }, set: { selected in +// if selected { +// selectedOptions.insert(strategy) +// } else { +// selectedOptions.remove(strategy) +// +// } +// })) {} +// } label: { +// Text(strategy.titleLabel()) +// Text(strategy.descriptionLabel()) +// } +// +// } +// } header: { +// Text("Stratégie d'importation") +// } +// } if convertingFile { Section { @@ -125,7 +123,7 @@ struct FileImportView: View { Text("Modifier la catégorie du tournoi ?") } .onChange(of: tournament.tournamentCategory) { - save() + _save() } } } else if teams.isEmpty && didImport == true { @@ -133,16 +131,24 @@ struct FileImportView: View { ContentUnavailableView("Aucune équipe détectée", systemImage: "person.2.slash") } } else if didImport { + let _filteredTeams = filteredTeams + let previousTeams = tournament.sortedTeams() + + if previousTeams.isEmpty == false { + Section { + TipView(notFoundAreWalkOutTip) + .tipStyle(tint: nil) + } + } Section { - let previousTeams = tournament.teams() - ForEach(filteredTeams) { team in + ForEach(_filteredTeams) { team in LabeledContent { HStack { if let previousTeam = team.previousTeam { Text(previousTeam.formattedSeed(in: previousTeams)) + Image(systemName: "arrowshape.forward.fill") } - Text("->") - Text(team.formattedSeed(in: filteredTeams)) + Text(team.formattedSeed(in: _filteredTeams)) } } label: { VStack(alignment: .leading) { @@ -153,9 +159,9 @@ struct FileImportView: View { } } header: { HStack { - Text("Équipes \(tournament.tournamentCategory.importingRawValue) détectées dans ce fichier") + Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)") Spacer() - Text(filteredTeams.count.formatted()) + Text(_filteredTeams.count.formatted()) } } } @@ -211,11 +217,8 @@ struct FileImportView: View { errorMessage = error.localizedDescription } } - .navigationTitle("Import") + .navigationTitle("Importation") .navigationBarTitleDisplayMode(.large) - .onDisappear { - //viewContext.rollback() - } .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Annuler", role: .cancel) { @@ -223,13 +226,13 @@ struct FileImportView: View { } } - ToolbarItem(placement: .bottomBar) { + ToolbarItem(placement: .topBarTrailing) { Button { - if selectedOptions.contains(.deleteBeforeImport) { // remove all previous teams + if false { //selectedOptions.contains(.deleteBeforeImport) try? dataStore.teamRegistrations.delete(contentOfs: tournament.unsortedTeams()) } - if selectedOptions.contains(.notFoundAreWalkOut) { + if true { //selectedOptions.contains(.notFoundAreWalkOut) let previousTeams = filteredTeams.compactMap({ $0.previousTeam }) let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams)) @@ -249,7 +252,8 @@ struct FileImportView: View { } label: { Text("Valider") } - .buttonStyle(.borderedProminent) + .buttonStyle(.bordered) + .clipShape(Capsule()) .disabled(teams.isEmpty) } } @@ -267,8 +271,8 @@ struct FileImportView: View { } } - func save() { - + private func _save() { + try? dataStore.tournaments.addOrUpdate(instance: tournament) } } diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift new file mode 100644 index 0000000..2677b8c --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -0,0 +1,191 @@ +// +// InscriptionInfoView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 27/03/2024. +// + +import SwiftUI + +struct InscriptionInfoView: View { + @Environment(Tournament.self) var tournament + @State private var duplicates = [PlayerRegistration]() + @State private var problematicPlayers = [PlayerRegistration]() + @State private var inadequatePlayers = [PlayerRegistration]() + @State private var playersWithoutValidLicense = [PlayerRegistration]() + @State private var entriesNotFromBeachPadel = [TeamRegistration]() + @State private var playersMissing = [TeamRegistration]() + @State private var waitingList = [TeamRegistration]() + @State private var selectedTeams = [TeamRegistration]() + + var body: some View { + List { + let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil }) + let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil }) + + Section { + DisclosureGroup { + ForEach(waitingListInBracket) { team in + TeamRowView(team: team) + } + } label: { + LabeledContent { + Text(waitingListInBracket.count.formatted()) + } label: { + Text("Dans le tableau") + } + } + .listRowView(color: .red) + + DisclosureGroup { + ForEach(waitingListInGroupStage) { team in + TeamRowView(team: team) + } + } label: { + LabeledContent { + Text(waitingListInGroupStage.count.formatted()) + } label: { + Text("En poule") + } + } + .listRowView(color: .red) + } header: { + Text("Équipes non sélectionnées") + } footer: { + Text("Il s'agit des équipes déjà placé en poule ou tableau qui sont actuellement en attente à cause de l'arrivée d'une nouvelle équipe ou une modification de classement.") + } + + Section { + DisclosureGroup { + ForEach(duplicates) { player in + ImportedPlayerView(player: player) + } + } label: { + LabeledContent { + Text(duplicates.count.formatted()) + } label: { + Text("Doublons") + } + } + .listRowView(color: .red) + } + + Section { + DisclosureGroup { + ForEach(problematicPlayers) { player in + PlayerSexPickerView(player: player) + } + } label: { + LabeledContent { + Text(problematicPlayers.count.formatted()) + } label: { + Text("Joueurs problématiques") + } + } + .listRowView(color: .purple) + } footer: { + Text("Il s'agit des joueurs ou joueuses dont le sexe n'a pas pu être déterminé") + } + + Section { + DisclosureGroup { + ForEach(inadequatePlayers) { player in + ImportedPlayerView(player: player) + } + } label: { + LabeledContent { + Text(inadequatePlayers.count.formatted()) + } label: { + let playerLabel : String = tournament.tournamentCategory == .women ? "joueuse" : "joueur" + let grammarSuffix : String = tournament.tournamentCategory == .women ? "e" + inadequatePlayers.count.pluralSuffix : inadequatePlayers.count.pluralSuffix + Text(playerLabel.capitalized + inadequatePlayers.count.pluralSuffix + " trop bien classé" + grammarSuffix) + } + } + .listRowView(color: .red) + } footer: { + Text("Il s'agit des joueurs ou joueuses dont le rang est inférieur à la limite fédérale.") + } + + Section { + DisclosureGroup { + ForEach(playersWithoutValidLicense) { + ImportedPlayerView(player: $0) + } + } label: { + LabeledContent { + Text(playersWithoutValidLicense.count.formatted()) + } label: { + Text("Joueurs sans licence valide") + } + } + .listRowView(color: .orange) + } footer: { + Text("importé du fichier beach-padel sans licence valide ou créé sans licence") + } + + Section { + DisclosureGroup { + ForEach(playersMissing) { + TeamRowView(team: $0) + } + } label: { + LabeledContent { + Text(playersMissing.count.formatted()) + } label: { + Text("Paires incomplètes") + } + } + .listRowView(color: .pink) + } + + Section { + LabeledContent { + Text(entriesNotFromBeachPadel.count.formatted()) + } label: { + Text("Paires importées") + Text(SourceFileManager.beachPadel.absoluteString) + } + .listRowView(color: .indigo) + + LabeledContent { + Text(selectedTeams.filter { $0.called() }.count.formatted()) + } label: { + Text("Paires convoquées") + Text("Vous avez envoyé une convocation par sms ou email") + } + .listRowView(color: .cyan) + + LabeledContent { + Text(selectedTeams.filter { $0.confirmed() }.count.formatted()) + } label: { + Text("Paires ayant confirmées") + Text("Vous avez noté la confirmation de l'équipe") + } + .listRowView(color: .green) + } + } + .navigationTitle("Synthèse") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .onAppear { + _initData() + } + } + + private func _initData() { + let players = tournament.unsortedPlayers() + selectedTeams = tournament.selectedSortedTeams() + waitingList = tournament.waitingListTeams(in: selectedTeams) + duplicates = tournament.duplicates(in: players) + problematicPlayers = players.filter({ $0.sex == -1 }) + inadequatePlayers = tournament.inadequatePlayers(in: players) + playersWithoutValidLicense = tournament.playersWithoutValidLicense(in: players) + entriesNotFromBeachPadel = selectedTeams.filter({ $0.isImported() }) + playersMissing = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) + } +} + +#Preview { + InscriptionInfoView() + .environment(Tournament.mock()) +} diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index c6cdbda..2741af4 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -145,18 +145,14 @@ struct InscriptionManagerView: View { } Divider() Button { - let count = tournament.unsortedTeams().count - if tournament.teamCount > count { - tournament.teamCount = count - } - tournament.closedRegistrationDate = Date() + tournament.lockRegistration() _save() } label: { Label("Clôturer", systemImage: "lock") } Divider() ShareLink(item: tournament.pasteDataForImporting()) { - Text("Exporter les paires") + Label("Exporter les paires", systemImage: "square.and.arrow.up") } Button { presentImportView = true @@ -196,30 +192,14 @@ struct InscriptionManagerView: View { private func _teamRegisteredView() -> some View { List { - Section { - _rankHandlerView() + let unfilteredTeams = tournament.sortedTeams() - let duplicates = tournament.duplicates() - DisclosureGroup { - if duplicates.isEmpty == false { - ForEach(duplicates) { player in - PlayerView(player: player) - } - } - } label: { - LabeledContent { - Text(duplicates.count.formatted()) - } label: { - Text("Doublons") - } - } - } header: { - Text("Informations") + if presentSearch == false { + _rankHandlerView() + _relatedTips() + _informationView(count: unfilteredTeams.count) } - _relatedTips() - - let unfilteredTeams = tournament.sortedTeams() let teams = searchField.isEmpty ? unfilteredTeams : unfilteredTeams.filter({ $0.contains(searchField.canonicalVersion) }) if teams.isEmpty && searchField.isEmpty == false { @@ -285,8 +265,12 @@ struct InscriptionManagerView: View { PasteButton(payloadType: String.self) { strings in guard let first = strings.first else { return } - fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) - pasteString = first + Task { + await MainActor.run { + fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) + pasteString = first + } + } } Button { @@ -422,9 +406,27 @@ struct InscriptionManagerView: View { } } + private func _informationView(count: Int) -> some View { + Section { + NavigationLink { + InscriptionInfoView() + .environment(tournament) + } label: { + LabeledContent { + Text(count.formatted() + "/" + tournament.teamCount.formatted()) + } label: { + Text("Analyse des inscriptions") + if let closedRegistrationDate = tournament.closedRegistrationDate { + Text("clôturé le " + closedRegistrationDate.formatted()) + } + } + } + } + } + @ViewBuilder private func _relatedTips() -> some View { - if pasteString?.isEmpty == true + if pasteString == nil && createdPlayerIds.isEmpty && tournament.unsortedTeams().count >= tournament.teamCount && tournament.unsortedPlayers().filter({ $0.source == .beachPadel }).isEmpty { diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 1354c4b..0ba0c87 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -14,7 +14,7 @@ struct TournamentCellView: View { var body: some View { HStack(alignment: .top) { - DateBoxView(date: Date()) + DateBoxView(date: tournament.startDate) Rectangle() .fill(color) .frame(width: 2) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 29f6373..21151a9 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -30,13 +30,31 @@ struct TournamentView: View { } - NavigationLink(value: Screen.inscription) { - LabeledContent { - Text(tournament.unsortedTeams().count.formatted()) - } label: { - Text("Inscriptions") + Section { + NavigationLink(value: Screen.inscription) { + LabeledContent { + Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted()) + } label: { + Text("Gestion des inscriptions") + if let closedRegistrationDate = tournament.closedRegistrationDate { + Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened)) + } + } + } + if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false { + LabeledContent { + Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened)) + } label: { + Text("Date limite") + } + + if endOfInscriptionDate < Date() { + RowButtonView(title: "Clôturer les inscriptions") { + tournament.lockRegistration() + _save() + } + } } - } switch tournament.state() { @@ -45,11 +63,6 @@ struct TournamentView: View { case .build: TournamentRunningView() } -// InscriptionManagerRowView(tournament: tournament) -// NavigationLink(value: Screen.groupStage) { -// Text("Poules") -// .badge(2) -// } } .toolbarBackground(.visible, for: .navigationBar) .navigationDestination(for: Screen.self, destination: { screen in @@ -102,15 +115,9 @@ struct TournamentView: View { } } } - - struct InscriptionManagerRowView: View { - let tournament: Tournament - var body: some View { - NavigationLink(value: Screen.inscription) { - Text("Inscriptions") - .badge(24) - } - } + + private func _save() { + try? dataStore.tournaments.addOrUpdate(instance: tournament) } } diff --git a/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift b/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift new file mode 100644 index 0000000..6c21d80 --- /dev/null +++ b/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift @@ -0,0 +1,33 @@ +// +// ListRowViewModifier.swift +// PadelClub +// +// Created by Razmig Sarkissian on 27/03/2024. +// + +import SwiftUI + +struct ListRowViewModifier: ViewModifier { + @State private var isActived = true + let color: Color + + func body(content: Content) -> some View { + if isActived { + content + .listRowBackground( + color.variation() + .overlay(alignment: .leading, content: { + color.frame(width: 8) + }) + ) + } else { + content + } + } +} + +extension View { + func listRowView(color: Color) -> some View { + modifier(ListRowViewModifier(color: color)) + } +}