From 5f4c995fb8821da65f9518b1d6a233cee7fe1fa5 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 17 Apr 2024 07:22:11 +0200 Subject: [PATCH 01/11] clean up --- PadelClub.xcodeproj/project.pbxproj | 16 +- PadelClub/Data/Tournament.swift | 39 ++-- PadelClub/Data/User.swift | 13 +- PadelClub/Manager/ContactManager.swift | 15 +- .../CallMessageCustomizationView.swift | 188 ++++++++++++++++++ .../Views/Calling/CallSettingsView.swift | 25 ++- .../CashierDetailView.swift | 0 .../Screen => Cashier}/CashierView.swift | 0 .../Views/Components/RowButtonView.swift | 2 +- PadelClub/Views/Event/EventCreationView.swift | 11 +- .../Event/TournamentConfiguratorView.swift | 12 +- .../Views/Planning/PlanningSettingsView.swift | 19 +- PadelClub/Views/Planning/PlanningView.swift | 109 ++-------- .../TournamentDurationManagerView.swift | 12 +- .../TournamentFieldsManagerView.swift | 11 +- .../Tournament/TournamentRunningView.swift | 12 +- 16 files changed, 308 insertions(+), 176 deletions(-) create mode 100644 PadelClub/Views/Calling/CallMessageCustomizationView.swift rename PadelClub/Views/{Tournament/Screen => Cashier}/CashierDetailView.swift (100%) rename PadelClub/Views/{Tournament/Screen => Cashier}/CashierView.swift (100%) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f613ca7..c4f6018 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ FF0EC5752BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-11-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5342BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-11-2022.csv */; }; FF0EC5762BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5452BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv */; }; FF0EC5772BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC54A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv */; }; + FF11627A2BCF8109000C4809 /* CallMessageCustomizationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162792BCF8109000C4809 /* CallMessageCustomizationView.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 */; }; @@ -354,6 +355,7 @@ FF0EC54A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-10-2022.csv"; sourceTree = ""; }; FF0EC54B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-11-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-11-2022.csv"; sourceTree = ""; }; FF0EC54C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-12-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-12-2022.csv"; sourceTree = ""; }; + FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageCustomizationView.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 = ""; }; @@ -642,6 +644,7 @@ FF089EB92BB011EE00F0AEC7 /* Player */, FF9267FD2BCE94520080F940 /* Calling */, FFF964512BC2628600EEF017 /* Planning */, + FF11627B2BCF937F000C4809 /* Cashier */, FF3F74F72B919F96004CFE0E /* Tournament */, C4A47D882B7BBB5000ADC637 /* Subscription */, C4A47D852B7BA33F00ADC637 /* User */, @@ -766,6 +769,15 @@ path = CSV; sourceTree = ""; }; + FF11627B2BCF937F000C4809 /* Cashier */ = { + isa = PBXGroup; + children = ( + FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */, + FF9267F72BCE78C70080F940 /* CashierView.swift */, + ); + path = Cashier; + sourceTree = ""; + }; FF1DC54D2BAB34FA00FD8220 /* Club */ = { isa = PBXGroup; children = ( @@ -825,8 +837,6 @@ FF8F26532BAE1E4400650388 /* TableStructureView.swift */, FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */, FF9268062BCE94D90080F940 /* TournamentCallView.swift */, - FF9267F72BCE78C70080F940 /* CashierView.swift */, - FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */, FF8F26522BAE0E4E00650388 /* Components */, ); path = Screen; @@ -950,6 +960,7 @@ FF9268002BCE94920080F940 /* SeedsCallingView.swift */, FF9268022BCE94A30080F940 /* GroupStageCallingView.swift */, FF9268082BCEDC2C0080F940 /* CallView.swift */, + FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */, ); path = Calling; sourceTree = ""; @@ -1362,6 +1373,7 @@ FF8F263D2BAD627A00650388 /* TournamentConfiguratorView.swift in Sources */, FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */, FF3B60A32BC49BBC008C2E66 /* MatchScheduler.swift in Sources */, + FF11627A2BCF8109000C4809 /* CallMessageCustomizationView.swift in Sources */, FF5DA1952BB927E800A33061 /* GenericDestinationPickerView.swift in Sources */, FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */, C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */, diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 67690a4..c1859cf 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -448,7 +448,7 @@ class Tournament : ModelObject, Storable { //todo var clubName: String? { - nil + eventObject?.clubObject?.name } //todo @@ -629,16 +629,11 @@ class Tournament : ModelObject, Storable { } func umpireMail() -> [String]? { - if let mail = UserDefaults.standard.string(forKey: "umpireMail"), mail.isEmpty == false { - return [mail] + if let email = DataStore.shared.user?.email { + return [email] } else { return nil } - -// if let umpireMail = federalTournament?.courrielEngagement { -// return [umpireMail] -// } else { -// } } func earnings() -> Double { @@ -655,19 +650,29 @@ class Tournament : ModelObject, Storable { return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count) } - func cashierStatus() -> String { - //todo - return "todo" + typealias TournamentStatus = (label:String, completion: String) + func cashierStatus() -> TournamentStatus { + let selectedPlayers = selectedPlayers() + let paid = selectedPlayers.filter({ $0.hasPaid() }) + let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés" + let completion = (Double(paid.count) / Double(selectedPlayers.count)).formatted(.percent.precision(.fractionLength(0))) + return TournamentStatus(label: label, completion: completion) } - func scheduleStatus() -> String { - //todo - return "todo" + func scheduleStatus() -> TournamentStatus { + let allMatches = allMatches() + let ready = allMatches.filter({ $0.startDate != nil }) + let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés" + let completion = (Double(ready.count) / Double(allMatches.count)).formatted(.percent.precision(.fractionLength(0))) + return TournamentStatus(label: label, completion: completion) } - func callStatus() -> String { - //todo - return "todo" + func callStatus() -> TournamentStatus { + let selectedSortedTeams = selectedSortedTeams() + let called = selectedSortedTeams.filter{ $0.called() } + let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " paires convoquées" + let completion = (Double(called.count) / Double(selectedSortedTeams.count)).formatted(.percent.precision(.fractionLength(0))) + return TournamentStatus(label: label, completion: completion) } func bracketStatus() -> String { diff --git a/PadelClub/Data/User.swift b/PadelClub/Data/User.swift index 1626594..63a3ac4 100644 --- a/PadelClub/Data/User.swift +++ b/PadelClub/Data/User.swift @@ -27,7 +27,12 @@ class User: UserBase { var lastName: String var phone: String? var country: String? - + var callMessageBody : String? = nil + var callMessageSignature: String? = nil + var callDisplayFormat: Bool = false + var callDisplayEntryFee: Bool = false + var callUseFullCustomMessage: Bool = false + init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?) { self.username = username self.firstName = firstName @@ -64,6 +69,12 @@ class User: UserBase { case _lastName = "lastName" case _phone = "phone" case _country = "country" + case _callMessageBody = "callMessageBody" + case _callMessageSignature = "callMessageSignature" + case _callDisplayFormat = "callDisplayFormat" + case _callDisplayEntryFee = "callDisplayEntryFee" + case _callUseFullCustomMessage = "callUseFullCustomMessage" + } } diff --git a/PadelClub/Manager/ContactManager.swift b/PadelClub/Manager/ContactManager.swift index d139a89..5c381cd 100644 --- a/PadelClub/Manager/ContactManager.swift +++ b/PadelClub/Manager/ContactManager.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import MessageUI +import LeStorage enum ContactManagerError: LocalizedError { case mailFailed @@ -33,7 +34,7 @@ extension ContactType { static let defaultSignature = "" static func callingGroupStageCustomMessage(tournament: Tournament?, startDate: Date?, roundLabel: String) -> String { - let tournamentCustomMessage = UserDefaults.standard.string(forKey: "customMessage") ?? defaultCustomMessage + let tournamentCustomMessage = DataStore.shared.user?.callMessageBody ?? defaultCustomMessage let clubName = tournament?.clubName ?? "" var text = tournamentCustomMessage @@ -48,7 +49,7 @@ extension ContactType { text = text.replacingOccurrences(of: "#jour", with: "\(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))") text = text.replacingOccurrences(of: "#horaire", with: "\(date.formatted(Date.FormatStyle().hour().minute()))") - let signature = UserDefaults.standard.string(forKey: "mySelf") ?? defaultSignature + let signature = DataStore.shared.user?.callMessageSignature ?? defaultSignature text = text.replacingOccurrences(of: "#signature", with: signature) return text @@ -56,7 +57,7 @@ extension ContactType { static func callingGroupStageMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?) -> String { - let useFullCustomMessage = UserDefaults.standard.bool(forKey: "useFullCustomMessage") + let useFullCustomMessage = DataStore.shared.user?.callUseFullCustomMessage ?? false if useFullCustomMessage { return callingGroupStageCustomMessage(tournament: tournament, startDate: startDate, roundLabel: roundLabel) @@ -65,17 +66,17 @@ extension ContactType { let date = startDate ?? tournament?.startDate ?? Date() let clubName = tournament?.clubName ?? "" - let message = UserDefaults.standard.string(forKey: "customMessage") ?? defaultCustomMessage - let signature = UserDefaults.standard.string(forKey: "mySelf") ?? defaultSignature + let message = DataStore.shared.user?.callMessageBody ?? defaultCustomMessage + let signature = DataStore.shared.user?.callMessageSignature ?? defaultSignature let localizedCalled = "convoqué" + (tournament?.tournamentCategory == .women ? "e" : "") + "s" var formatMessage: String? { - UserDefaults.standard.bool(forKey: "displayFormat") ? matchFormat?.computedLongLabel.appending(".") : nil + (DataStore.shared.user?.callDisplayFormat ?? false) ? matchFormat?.computedLongLabel.appending(".") : nil } var entryFeeMessage: String? { - UserDefaults.standard.bool(forKey: "displayEntryFee") ? tournament?.entryFeeMessage : nil + (DataStore.shared.user?.callDisplayEntryFee ?? false) ? tournament?.entryFeeMessage : nil } var computedMessage: String { diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift new file mode 100644 index 0000000..6d8f309 --- /dev/null +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -0,0 +1,188 @@ +// +// CallMessageCustomizationView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 02/11/2023. +// + +import SwiftUI + +struct CallMessageCustomizationView: View { + @EnvironmentObject var dataStore: DataStore + var tournament: Tournament + var user: User + + @FocusState private var textEditor: Bool + @State private var customClubName: String = "" + @State private var customCallMessageBody: String = "" + @State private var customCallMessageSignature: String = "" + + init(tournament: Tournament, user: User) { + self.tournament = tournament + self.user = user + _customCallMessageBody = State(wrappedValue: user.callMessageBody ?? "") + _customCallMessageSignature = State(wrappedValue: user.callMessageSignature ?? "") + _customClubName = State(wrappedValue: tournament.clubName ?? "") + } + + var clubName: String { + customClubName + } + + var formatMessage: String? { + user.callDisplayFormat ? tournament.matchFormat.computedLongLabel + "." : nil + } + + var entryFeeMessage: String? { + user.callDisplayEntryFee ? tournament.entryFeeMessage : nil + } + + var computedMessage: String { + [formatMessage, entryFeeMessage, customCallMessageBody].compacted().map { $0.trimmed }.joined(separator: "\n") + } + + var finalMessage: String? { + let localizedCalled = "convoqué" + (tournament.tournamentCategory == .women ? "e" : "") + "s" + return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(RoundRule.roundName(fromRoundIndex: 2).lowercased()) du \(tournament.tournamentTitle()) au \(clubName) le \(Date().formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(Date().formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)" + } + + var body: some View { + @Bindable var user = user + List { + Section { + ZStack { + Text(customCallMessageBody).hidden() + .padding(.vertical, 20) + TextEditor(text: $customCallMessageBody) + .autocorrectionDisabled() + .focused($textEditor) + } + } header: { + Text("Personnalisation du message de convocation") + } + + Section { + ZStack { + Text(customCallMessageSignature).hidden() + TextEditor(text: $customCallMessageSignature) + .autocorrectionDisabled() + .focused($textEditor) + } + } header: { + Text("Signature du message") + } + + Section { + TextField("Nom du club", text: $customClubName) + .autocorrectionDisabled() + .onSubmit { + if let eventClub = tournament.eventObject?.clubObject { + eventClub.name = customClubName + try? dataStore.clubs.addOrUpdate(instance: eventClub) + } + } + } header: { + Text("Nom du club") + } + + Section { + if user.callUseFullCustomMessage { + Text(self.computedFullCustomMessage()) + .contextMenu { + Button("Coller dans le presse-papier") { + UIPasteboard.general.string = self.computedFullCustomMessage() + } + } + } + else if let finalMessage { + Text(finalMessage) + .contextMenu { + Button("Coller dans le presse-papier") { + UIPasteboard.general.string = finalMessage + } + } + } + } header: { + Text("Exemple") + } + + Section { + LabeledContent { + Toggle(isOn: $user.callUseFullCustomMessage) { + + } + } label: { + Text("contrôle complet du message") + } + } header: { + Text("Personnalisation complète") + } footer: { + Text("Utilisez ces balises dans votre texte : #titre, #jour, #horaire, #club, #signature") + } + } + .navigationTitle("Message de convocation") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Picker(selection: $user.callDisplayFormat) { + Text("Afficher le format").tag(true) + Text("Masquer le format").tag(false) + } label: { + + } + Picker(selection: $user.callDisplayEntryFee) { + Text("Afficher le prix d'inscription").tag(true) + Text("Masquer le prix d'inscription").tag(false) + } label: { + + } + } label: { + LabelOptions() + } + } + ToolbarItemGroup(placement: .keyboard) { + if textEditor { + Spacer() + Button { + textEditor = false + } label: { + Label("Fermer", systemImage: "xmark") + } + } + } + } + .onChange(of: user.callUseFullCustomMessage) { + if user.callUseFullCustomMessage == false { + user.callMessageBody = ContactType.defaultCustomMessage + } + _save() + } + .onChange(of: customCallMessageBody) { + user.callMessageBody = customCallMessageBody + _save() + } + .onChange(of: customCallMessageSignature) { + user.callMessageSignature = customCallMessageSignature + _save() + } + .onChange(of: user.callDisplayEntryFee) { + _save() + } + .onChange(of: user.callDisplayFormat) { + _save() + } + } + + private func _save() { + try? dataStore.setUser(user) + } + + func computedFullCustomMessage() -> String { + var text = customCallMessageBody.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle()) + text = text.replacingOccurrences(of: "#club", with: clubName) + text = text.replacingOccurrences(of: "#jour", with: "\(Date().formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))") + text = text.replacingOccurrences(of: "#horaire", with: "\(Date().formatted(Date.FormatStyle().hour().minute()))") + text = text.replacingOccurrences(of: "#signature", with: customCallMessageSignature) + return text + } +} diff --git a/PadelClub/Views/Calling/CallSettingsView.swift b/PadelClub/Views/Calling/CallSettingsView.swift index f794e23..48d868e 100644 --- a/PadelClub/Views/Calling/CallSettingsView.swift +++ b/PadelClub/Views/Calling/CallSettingsView.swift @@ -14,15 +14,24 @@ struct CallSettingsView: View { var body: some View { List { + if let user = dataStore.user { + Section { + NavigationLink { + CallMessageCustomizationView(tournament: tournament, user: user) + } label: { + Text("Personnaliser le message de convocation") + } + } + } + Section { - NavigationLink { - } label: { - Text("Modifier le message de convocation") + RowButtonView("Envoyer un message à tout le monde") { + } } Section { - RowButtonView("Annuler toutes les convocations") { + RowButtonView("Annuler toutes les convocations", role: .destructive) { let teams = tournament.unsortedTeams() teams.forEach { team in team.callDate = nil @@ -32,13 +41,7 @@ struct CallSettingsView: View { } Section { - RowButtonView("Envoyer un message à tout le monde") { - - } - } - - Section { - RowButtonView("Tout le monde a été convoqué") { + RowButtonView("Tout le monde a été convoqué", role: .destructive) { let teams = tournament.unsortedTeams() teams.forEach { team in team.callDate = Date() diff --git a/PadelClub/Views/Tournament/Screen/CashierDetailView.swift b/PadelClub/Views/Cashier/CashierDetailView.swift similarity index 100% rename from PadelClub/Views/Tournament/Screen/CashierDetailView.swift rename to PadelClub/Views/Cashier/CashierDetailView.swift diff --git a/PadelClub/Views/Tournament/Screen/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift similarity index 100% rename from PadelClub/Views/Tournament/Screen/CashierView.swift rename to PadelClub/Views/Cashier/CashierView.swift diff --git a/PadelClub/Views/Components/RowButtonView.swift b/PadelClub/Views/Components/RowButtonView.swift index b63c450..b8c3b34 100644 --- a/PadelClub/Views/Components/RowButtonView.swift +++ b/PadelClub/Views/Components/RowButtonView.swift @@ -66,7 +66,7 @@ struct RowButtonView: View { .disabled(animatedProgress) .frame(maxWidth: .infinity) .buttonStyle(.borderedProminent) - .tint(role == .destructive ? Color.red : Color.launchScreenBackground) + .tint(role == .destructive ? Color.red : Color.master) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets(.zero)) .confirmationDialog("Confirmation", diff --git a/PadelClub/Views/Event/EventCreationView.swift b/PadelClub/Views/Event/EventCreationView.swift index 3b9670f..c908431 100644 --- a/PadelClub/Views/Event/EventCreationView.swift +++ b/PadelClub/Views/Event/EventCreationView.swift @@ -40,12 +40,11 @@ struct EventCreationView: View { } if eventType == .approvedTournament { - Stepper(value: $duration, in: 1...3) { - HStack { - Text("Durée") - Spacer() - Text("\(duration) jour" + duration.pluralSuffix) - } + LabeledContent { + StepperView(count: $duration, minimum: 1, maximum: 3) + } label: { + Text("Durée") + Text("\(duration) jour" + duration.pluralSuffix) } } diff --git a/PadelClub/Views/Event/TournamentConfiguratorView.swift b/PadelClub/Views/Event/TournamentConfiguratorView.swift index 1f1231c..222fdaf 100644 --- a/PadelClub/Views/Event/TournamentConfiguratorView.swift +++ b/PadelClub/Views/Event/TournamentConfiguratorView.swift @@ -35,13 +35,11 @@ struct TournamentConfigurationView: View { Text(type.localizedLabel()).tag(type.rawValue) } } - - Stepper(value: $tournament.teamCount, in: minimumTeamsCount...maximumTeamsCount) { - HStack { - Text("Équipes souhaitées") - Spacer() - Text(tournament.teamCount.formatted()) - } + LabeledContent { + StepperView(count: $tournament.teamCount, minimum: minimumTeamsCount, maximum: maximumTeamsCount) + } label: { + Text("Équipes souhaitées") + Text(tournament.teamCount.formatted()) } } } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 69e9559..0721e80 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -39,12 +39,11 @@ struct PlanningSettingsView: View { List { Section { DatePicker(tournament.startDate.formatted(.dateTime.weekday()), selection: $tournament.startDate) - Stepper(value: $tournament.dayDuration, in: 1...1_000) { - HStack { - Text("Durée") - Spacer() - Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) - } + LabeledContent { + StepperView(count: $tournament.dayDuration, minimum: 1, maximum: 1_000) + } label: { + Text("Durée") + Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) } } header: { Text("Démarrage et durée du tournoi") @@ -114,14 +113,6 @@ struct PlanningSettingsView: View { } } } - - Section { - NavigationLink { - - } label: { - Text("Modifier le message de convocation") - } - } } .onChange(of: groupStageCourtCount) { tournament.groupStageCourtCount = groupStageCourtCount diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index 6214d5b..0d688fd 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -9,7 +9,6 @@ import SwiftUI struct PlanningView: View { @EnvironmentObject var dataStore: DataStore - @Environment(\.editMode) private var editMode let matches: [Match] @State private var timeSlots: [Date:[Match]] @@ -30,85 +29,27 @@ struct PlanningView: View { Section { ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in if let _matches = timeSlots[key] { - if editMode?.wrappedValue.isEditing == true { - HStack { - VStack(alignment: .leading) { - let index = keys.firstIndex(of: key) - Button { - let previousKey = keys[index! - 1] - let previousMatches = timeSlots[previousKey] - previousMatches?.forEach { match in - match.startDate = key - } - _matches.forEach { match in - match.startDate = previousKey - } - _update() - } label: { - Image(systemName: "arrow.up") - } - .buttonStyle(.bordered) - .disabled(index == 0) - Button { - let nextKey = keys[index! + 1] - let nextMatches = timeSlots[nextKey] - nextMatches?.forEach { match in - match.startDate = key - } - _matches.forEach { match in - match.startDate = nextKey - } - _update() - } label: { - Image(systemName: "arrow.down") - } - .buttonStyle(.bordered) - .disabled(index == keys.count - 1) - } - VStack(alignment: .leading) { + DisclosureGroup { + ForEach(_matches) { match in + NavigationLink { + MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) + } label: { LabeledContent { - Text(_matches.count.formatted() + " match" + _matches.count.pluralSuffix) - } label: { - Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle) - } - - ForEach(_matches) { match in - LabeledContent { - Text(match.matchFormat.format) - } label: { - if let groupStage = match.groupStageObject { - Text(groupStage.groupStageTitle()) - } else if let round = match.roundObject { - Text(round.roundTitle()) - } - Text(match.matchTitle()) + if let court = match.court { + Text(court) } - } - } - } - } else { - DisclosureGroup { - ForEach(_matches) { match in - NavigationLink { - MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) } label: { - LabeledContent { - if let court = match.court { - Text(court) - } - } label: { - if let groupStage = match.groupStageObject { - Text(groupStage.groupStageTitle()) - } else if let round = match.roundObject { - Text(round.roundTitle()) - } - Text(match.matchTitle()) + if let groupStage = match.groupStageObject { + Text(groupStage.groupStageTitle()) + } else if let round = match.roundObject { + Text(round.roundTitle()) } + Text(match.matchTitle()) } } - } label: { - _timeSlotView(key: key, matches: _matches) } + } label: { + _timeSlotView(key: key, matches: _matches) } } } @@ -118,29 +59,9 @@ struct PlanningView: View { .headerProminence(.increased) } } - .toolbar { - EditButton() - } - .onChange(of: isEditing) { old, new in - if old == true && new == false { - print("save") - try? dataStore.matches.addOrUpdate(contentOfs: matches) - } - } .navigationTitle("Programmation") } - - private func _update() { - let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } - self.timeSlots = timeSlots - self.days = Set(timeSlots.keys.map { $0.startOfDay }).sorted() - self.keys = timeSlots.keys.sorted() - } - - private var isEditing: Bool { - editMode?.wrappedValue.isEditing == true - } - + private func _timeSlotView(key: Date, matches: [Match]) -> some View { LabeledContent { Text(matches.count.formatted() + " match" + matches.count.pluralSuffix) diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentDurationManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentDurationManagerView.swift index f6e5b78..dd4887d 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentDurationManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentDurationManagerView.swift @@ -12,13 +12,11 @@ struct TournamentDurationManagerView: View { var body: some View { @Bindable var tournament = tournament - - Stepper(value: $tournament.dayDuration, in: 1...3) { - LabeledContent { - Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) - } label: { - Text("Durée") - } + LabeledContent { + StepperView(count: $tournament.dayDuration, minimum: 1, maximum: 3) + } label: { + Text("Durée") + Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) } } } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift index 8f46f49..56bf2d7 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift @@ -12,12 +12,11 @@ struct TournamentFieldsManagerView: View { @Binding var count: Int var body: some View { - Stepper(value: $count, in: 1...1_000) { - LabeledContent { - Text(count.formatted()) - } label: { - Text(localizedStringKey) - } + LabeledContent { + StepperView(count: $count, minimum: 1, maximum: 1_000) + } label: { + Text(localizedStringKey) + Text(count.formatted()) } } } diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index fad5286..3d1194c 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -14,26 +14,32 @@ struct TournamentRunningView: View { var body: some View { Section { NavigationLink(value: Screen.schedule) { + let tournamentStatus = tournament.scheduleStatus() LabeledContent { - Text(tournament.scheduleStatus()) + Text(tournamentStatus.completion).foregroundStyle(.master) } label: { Text("Horaires") + Text(tournamentStatus.label) } } NavigationLink(value: Screen.call) { + let tournamentStatus = tournament.callStatus() LabeledContent { - Text(tournament.callStatus()) + Text(tournamentStatus.completion).foregroundStyle(.master) } label: { Text("Convocations") + Text(tournamentStatus.label) } } NavigationLink(value: Screen.cashier) { + let tournamentStatus = tournament.cashierStatus() LabeledContent { - Text(tournament.cashierStatus()) + Text(tournamentStatus.completion).foregroundStyle(.master) } label: { Text("Encaissement") + Text(tournamentStatus.label) } } } From 455074c155d950a87552b46abe1136d070ccf867 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 17 Apr 2024 11:14:22 +0200 Subject: [PATCH 02/11] 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) } From d34967df33dc8267e38671a2972067d09ce9ce66 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 17 Apr 2024 15:12:57 +0200 Subject: [PATCH 03/11] clean up inscription --- PadelClub/Data/TeamRegistration.swift | 15 +++++ PadelClub/Data/Tournament.swift | 15 +++++ PadelClub/Manager/FileImportManager.swift | 10 ++- PadelClub/Manager/PadelRule.swift | 4 ++ .../ViewModel/TournamentSeedEditing.swift | 13 +--- PadelClub/Views/Calling/CallView.swift | 6 +- .../Views/Calling/GroupStageCallingView.swift | 7 +- .../Views/Calling/SeedsCallingView.swift | 2 +- PadelClub/Views/Match/MatchRowView.swift | 4 +- PadelClub/Views/Round/LoserRoundsView.swift | 8 ++- PadelClub/Views/Round/RoundSettingsView.swift | 4 +- PadelClub/Views/Round/RoundView.swift | 6 +- PadelClub/Views/Round/RoundsView.swift | 6 +- PadelClub/Views/Team/TeamDetailView.swift | 6 +- PadelClub/Views/Team/TeamRowView.swift | 2 +- .../Views/Tournament/FileImportView.swift | 55 ++++++++++------ .../Components/InscriptionInfoView.swift | 64 ++++++++++++------- .../Screen/InscriptionManagerView.swift | 9 ++- 18 files changed, 155 insertions(+), 81 deletions(-) diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 0f42cb0..b0a9453 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -283,6 +283,21 @@ class TeamRegistration: ModelObject, Storable { return Store.main.findById(groupStage) } + func initialRound() -> Round? { + guard let bracketPosition else { return nil } + let matchIndex = RoundRule.matchIndex(fromBracketPosition: bracketPosition) + let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2) + return Store.main.filter(isIncluded: { $0.tournament == tournament && $0.index == roundIndex }).first + } + + func initialMatch() -> Match? { + guard let bracketPosition else { return nil } + guard let initialRoundObject = initialRound() else { return nil } + let matchIndex = RoundRule.matchIndex(fromBracketPosition: bracketPosition) + return Store.main.filter(isIncluded: { $0.round == initialRoundObject.id && $0.index == matchIndex }).first + } + + func tournamentObject() -> Tournament? { Store.main.findById(tournament) } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 50682bd..e2be5d8 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -506,6 +506,11 @@ class Tournament : ModelObject, Storable { 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 getStartDate(ofSeedIndex seedIndex: Int?) -> Date? { + guard let seedIndex else { return nil } + return selectedSortedTeams()[safe: seedIndex]?.callDate + } + func importTeams(_ teams: [FileImportManager.TeamHolder]) { var teamsToImport = [TeamRegistration]() teams.forEach { team in @@ -523,6 +528,16 @@ class Tournament : ModelObject, Storable { } + func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool { + guard let callDate = team.callDate else { return false } + if let groupStageStartDate = team.groupStageObject()?.startDate { + return Calendar.current.compare(callDate, to: groupStageStartDate, toGranularity: .minute) != ComparisonResult.orderedSame + } else if let roundMatchStartDate = team.initialMatch()?.startDate { + return Calendar.current.compare(callDate, to: roundMatchStartDate, toGranularity: .minute) != ComparisonResult.orderedSame + } + return false + } + func lockRegistration() { closedRegistrationDate = Date() let count = selectedSortedTeams().count diff --git a/PadelClub/Manager/FileImportManager.swift b/PadelClub/Manager/FileImportManager.swift index ac224b4..3810e7e 100644 --- a/PadelClub/Manager/FileImportManager.swift +++ b/PadelClub/Manager/FileImportManager.swift @@ -79,7 +79,15 @@ class FileImportManager { func index(in teams: [TeamHolder]) -> Int? { teams.firstIndex(where: { $0.id == id }) } - + + func formattedSeedIndex(index: Int?) -> String { + if let index { + return "#\(index + 1)" + } else { + return "###" + } + } + func formattedSeed(in teams: [TeamHolder]) -> String { if let index = index(in: teams) { return "#\(index + 1)" diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index cd55595..71a5c96 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -1419,6 +1419,10 @@ enum RoundRule { return (1 << roundIndex) - 1 } + static func matchIndex(fromBracketPosition: Int) -> Int { + roundIndex(fromMatchIndex: fromBracketPosition / 2) + fromBracketPosition%2 + } + static func roundIndex(fromMatchIndex matchIndex: Int) -> Int { Int(log2(Double(matchIndex + 1))) } diff --git a/PadelClub/ViewModel/TournamentSeedEditing.swift b/PadelClub/ViewModel/TournamentSeedEditing.swift index 4519355..5f4964b 100644 --- a/PadelClub/ViewModel/TournamentSeedEditing.swift +++ b/PadelClub/ViewModel/TournamentSeedEditing.swift @@ -8,22 +8,13 @@ import Foundation import SwiftUI -// Create an environment key private struct TournamentSeedEditing: EnvironmentKey { - static let defaultValue: Bool = false + static let defaultValue: Binding = .constant(false) } -// ## Introduce new value to EnvironmentValues extension EnvironmentValues { - var isEditingTournamentSeed: Bool { + var isEditingTournamentSeed: Binding { get { self[TournamentSeedEditing.self] } set { self[TournamentSeedEditing.self] = newValue } } } - -// Add a dedicated modifier (Optional) -extension View { - func editTournamentSeed(_ value: Bool) -> some View { - environment(\.isEditingTournamentSeed, value) - } -} diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index 4abd713..84282d9 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -86,7 +86,11 @@ struct CallView: View { let callWord = teams.allSatisfy({ $0.called() }) ? "Reconvoquer" : "Convoquer" HStack { if teams.count == 1 { - Text(callWord + " cette paire par") + if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { + Text("Reconvoquer " + callDate.localizedDate() + " par") + } else { + Text(callWord + " cette paire par") + } } else { Text(callWord + " ces \(teams.count) paires par") } diff --git a/PadelClub/Views/Calling/GroupStageCallingView.swift b/PadelClub/Views/Calling/GroupStageCallingView.swift index c401f3e..7b003c6 100644 --- a/PadelClub/Views/Calling/GroupStageCallingView.swift +++ b/PadelClub/Views/Calling/GroupStageCallingView.swift @@ -19,8 +19,8 @@ struct GroupStageCallingView: View { ForEach(groupStages) { groupStage in let seeds = groupStage.teams() - let callSeeds = seeds.filter({ $0.callDate != nil }) - + let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) + if seeds.isEmpty == false { Section { NavigationLink { @@ -51,8 +51,9 @@ struct GroupStageCallingView: View { ForEach(keys, id: \.self) { key in if let _groupStages = times[key], _groupStages.count > 1 { let teams = _groupStages.flatMap { $0.teams() } + let callSeeds = teams.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) Section { - CallView.CallStatusView(count: teams.filter({ $0.callDate != nil }).count, total: teams.count, startDate: key) + CallView.CallStatusView(count: callSeeds.count, total: teams.count, startDate: key) } header: { Text(groupStages.map { $0.groupStageTitle() }.joined(separator: ", ")) } footer: { diff --git a/PadelClub/Views/Calling/SeedsCallingView.swift b/PadelClub/Views/Calling/SeedsCallingView.swift index e41e049..d3b0400 100644 --- a/PadelClub/Views/Calling/SeedsCallingView.swift +++ b/PadelClub/Views/Calling/SeedsCallingView.swift @@ -15,7 +15,7 @@ struct SeedsCallingView: View { List { ForEach(tournament.rounds()) { round in let seeds = round.seeds() - let callSeeds = seeds.filter({ $0.callDate != nil }) + let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) if seeds.isEmpty == false { Section { NavigationLink { diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index 79e1e3b..0251cbb 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -10,11 +10,11 @@ import SwiftUI struct MatchRowView: View { var match: Match let matchViewStyle: MatchViewStyle - @Environment(\.editMode) private var editMode + @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @ViewBuilder var body: some View { - if editMode?.wrappedValue.isEditing == true && match.isGroupStage() == false && match.isLoserBracket == false { + if isEditingTournamentSeed.wrappedValue == true && match.isGroupStage() == false && match.isLoserBracket == false { MatchSetupView(match: match) } else { NavigationLink { diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index fd9dd32..9c86850 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -40,7 +40,7 @@ struct LoserRoundsView: View { struct LoserRoundView: View { @EnvironmentObject var dataStore: DataStore let loserRounds: [Round] - @Environment(\.editMode) private var editMode + @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed private func _roundDisabled() -> Bool { loserRounds.allSatisfy({ $0.isDisabled() }) @@ -48,7 +48,7 @@ struct LoserRoundView: View { var body: some View { List { - if editMode?.wrappedValue.isEditing == true { + if isEditingTournamentSeed.wrappedValue == true { _editingView() } @@ -73,7 +73,9 @@ struct LoserRoundView: View { } .headerProminence(.increased) .toolbar { - EditButton() + Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { + isEditingTournamentSeed.wrappedValue.toggle() + } } } diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 8a412a0..8fd0963 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -9,7 +9,7 @@ import SwiftUI struct RoundSettingsView: View { @EnvironmentObject var dataStore: DataStore - @Environment(\.editMode) private var editMode + @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @Environment(Tournament.self) var tournament: Tournament var body: some View { @@ -21,7 +21,7 @@ struct RoundSettingsView: View { tournament.allRounds().forEach({ round in round.enableRound() }) - editMode?.wrappedValue = .active + self.isEditingTournamentSeed.wrappedValue = true } } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index cc1cb11..ffa25a3 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -8,7 +8,7 @@ import SwiftUI struct RoundView: View { - @Environment(\.editMode) private var editMode + @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @Environment(Tournament.self) var tournament: Tournament @EnvironmentObject var dataStore: DataStore @@ -17,7 +17,7 @@ struct RoundView: View { var body: some View { List { - if editMode?.wrappedValue.isEditing == false { + if isEditingTournamentSeed.wrappedValue == false { let loserRounds = round.loserRounds() if loserRounds.isEmpty == false, let first = loserRounds.first(where: { $0.isDisabled() == false }) { Section { @@ -37,7 +37,7 @@ struct RoundView: View { try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds()) if tournament.availableSeeds().isEmpty { - editMode?.wrappedValue = .inactive + self.isEditingTournamentSeed.wrappedValue = false } } } diff --git a/PadelClub/Views/Round/RoundsView.swift b/PadelClub/Views/Round/RoundsView.swift index 1134fed..db3b83d 100644 --- a/PadelClub/Views/Round/RoundsView.swift +++ b/PadelClub/Views/Round/RoundsView.swift @@ -10,13 +10,13 @@ import SwiftUI struct RoundsView: View { var tournament: Tournament @State private var selectedRound: Round? - @State var editMode: EditMode = .inactive + @State private var isEditingTournamentSeed = false init(tournament: Tournament) { self.tournament = tournament _selectedRound = State(wrappedValue: tournament.getActiveRound()) if tournament.availableSeeds().isEmpty == false { - _editMode = .init(wrappedValue: .active) + _isEditingTournamentSeed = State(wrappedValue: true) } } @@ -32,7 +32,7 @@ struct RoundsView: View { .navigationTitle(selectedRound.roundTitle()) } } - .environment(\.editMode, $editMode) + .environment(\.isEditingTournamentSeed, $isEditingTournamentSeed) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) } diff --git a/PadelClub/Views/Team/TeamDetailView.swift b/PadelClub/Views/Team/TeamDetailView.swift index fce9049..cff5a51 100644 --- a/PadelClub/Views/Team/TeamDetailView.swift +++ b/PadelClub/Views/Team/TeamDetailView.swift @@ -16,7 +16,11 @@ struct TeamDetailView: View { Text("Aucun joueur, espace réservé") } else { ForEach(team.players()) { player in - PlayerView(player: player) + NavigationLink { + Text("Hello wolrd") + } label: { + PlayerView(player: player) + } } } } diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 1b02d36..4eebb7e 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -31,7 +31,7 @@ struct TeamRowView: View { } } label: { Text(team.teamLabel(.short)) - if let callDate = team.callDate { + if let callDate = team.callDate, displayCallDate { Text("Déjà convoquée \(callDate.localizedDate())") .foregroundStyle(.red) .italic() diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 0974f39..9eeb438 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -141,29 +141,16 @@ struct FileImportView: View { } } Section { - ForEach(_filteredTeams) { team in - LabeledContent { - HStack { - if let previousTeam = team.previousTeam { - Text(previousTeam.formattedSeed(in: previousTeams)) - Image(systemName: "arrowshape.forward.fill") - } - Text(team.formattedSeed(in: _filteredTeams)) - } - } label: { - VStack(alignment: .leading) { - Text(team.playerOne.playerLabel()) - Text(team.playerTwo.playerLabel()) - } - } - } - } header: { - HStack { - Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)") - Spacer() + LabeledContent { Text(_filteredTeams.count.formatted()) + } label: { + Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)") } } + + ForEach(_filteredTeams) { team in + _teamView(team: team, inTeams: _filteredTeams, previousTeams: previousTeams) + } } } .onAppear { @@ -272,6 +259,34 @@ struct FileImportView: View { didImport = true } } + + @ViewBuilder + private func _teamView(team: FileImportManager.TeamHolder, inTeams teams: [FileImportManager.TeamHolder], previousTeams: [TeamRegistration]) -> some View { + + let newIndex = team.index(in: teams) + Section { + HStack { + VStack(alignment: .leading) { + Text(team.playerOne.playerLabel()) + Text(team.playerTwo.playerLabel()) + } + Spacer() + HStack { + if let previousTeam = team.previousTeam { + Text(previousTeam.formattedSeed(in: previousTeams)) + Image(systemName: "arrowshape.forward.fill") + } + Text(team.formattedSeedIndex(index: newIndex)) + } + } + if let callDate = team.previousTeam?.callDate, let newDate = tournament.getStartDate(ofSeedIndex: newIndex), callDate != newDate { + Text("Attention, cette paire a déjà été convoquée à \(callDate.localizedDate())") + .foregroundStyle(.red) + .italic() + .font(.caption) + } + } + } 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 index 02c6d7d..5c9b896 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -10,14 +10,21 @@ import SwiftUI struct InscriptionInfoView: View { @EnvironmentObject var dataStore: DataStore @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 entriesFromBeachPadel = [TeamRegistration]() - @State private var playersMissing = [TeamRegistration]() - @State private var waitingList = [TeamRegistration]() - @State private var selectedTeams = [TeamRegistration]() + + var players : [PlayerRegistration] { tournament.unsortedPlayers() } + var selectedTeams : [TeamRegistration] { tournament.selectedSortedTeams() } + + var callDateIssue : [TeamRegistration] { + selectedTeams.filter { tournament.isStartDateIsDifferentThanCallDate($0) } + } + + var waitingList : [TeamRegistration] { tournament.waitingListTeams(in: selectedTeams) } + var duplicates : [PlayerRegistration] { tournament.duplicates(in: players) } + var problematicPlayers : [PlayerRegistration] { players.filter({ $0.sex == -1 }) } + var inadequatePlayers : [PlayerRegistration] { tournament.inadequatePlayers(in: players) } + var playersWithoutValidLicense : [PlayerRegistration] { tournament.playersWithoutValidLicense(in: players) } + var entriesFromBeachPadel : [TeamRegistration] { tournament.unsortedTeams().filter({ $0.isImported() }) } + var playersMissing : [TeamRegistration] { selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) } var body: some View { List { @@ -47,6 +54,30 @@ struct InscriptionInfoView: View { .listRowView(color: .green) } + Section { + DisclosureGroup { + ForEach(callDateIssue) { team in + CallView.TeamView(team: team) + if let groupStage = team.groupStageObject(), let callDate = groupStage.startDate { + CallView(teams: [team], callDate: callDate, matchFormat: groupStage.matchFormat, roundLabel: "poule") + } else if let initialRound = team.initialRound(), + let initialMatch = team.initialMatch(), + let callDate = initialMatch.startDate { + CallView(teams: [team], callDate: callDate, matchFormat: initialMatch.matchFormat, roundLabel: initialRound.roundTitle()) + } + } + } label: { + LabeledContent { + Text(callDateIssue.count.formatted()) + } label: { + Text("Erreur de convocation") + } + } + .listRowView(color: .brown) + } footer: { + Text("L'horaire de la convocation est différente du match initial") + } + let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil }) let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil }) @@ -166,22 +197,7 @@ struct InscriptionInfoView: View { .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) - entriesFromBeachPadel = tournament.unsortedTeams().filter({ $0.isImported() }) - playersMissing = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) - } + } } #Preview { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 230fca8..041f9ec 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -739,7 +739,7 @@ struct InscriptionManagerView: View { private func _teamFooterView(_ team: TeamRegistration) -> some View { HStack { if let formattedRegistrationDate = team.formattedInscriptionDate() { - Text(formattedRegistrationDate).font(.caption).foregroundStyle(.secondary) + Text(formattedRegistrationDate).foregroundStyle(.secondary) } Spacer() _teamMenuOptionView(team) @@ -748,7 +748,7 @@ struct InscriptionManagerView: View { private func _teamMenuOptionView(_ team: TeamRegistration) -> some View { Menu { Section { - Button("Éditer les joueurs") { + Button("Modifier l'équipe") { editedTeam = team team.unsortedPlayers().forEach { player in createdPlayers.insert(player) @@ -799,12 +799,11 @@ struct InscriptionManagerView: View { } label: { LabelDelete() } - } header: { - Text(team.teamLabel(.short)) +// } header: { +// Text(team.teamLabel(.short)) } } label: { LabelOptions().labelStyle(.titleOnly) - .font(.caption) } } From b1db99f2a0961df3f2b1518c6d9ded98369a815e Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 17 Apr 2024 20:23:14 +0200 Subject: [PATCH 04/11] fix call view fix schedule view clean up --- PadelClub.xcodeproj/project.pbxproj | 8 ++ PadelClub/Data/GroupStage.swift | 17 ++- PadelClub/Data/PlayerRegistration.swift | 6 +- PadelClub/Data/Round.swift | 4 + PadelClub/Data/TeamRegistration.swift | 4 +- PadelClub/Data/Tournament.swift | 24 ++++ PadelClub/ViewModel/AgendaDestination.swift | 4 + PadelClub/ViewModel/Selectable.swift | 1 + .../Views/Calling/GroupStageCallingView.swift | 23 ++-- .../GenericDestinationPickerView.swift | 13 +- .../Views/Components/RowButtonView.swift | 81 ++++++++----- .../Views/GroupStage/GroupStagesView.swift | 4 + PadelClub/Views/Navigation/MainView.swift | 10 +- .../GroupStageScheduleEditorView.swift | 55 +++++++-- .../LoserRoundScheduleEditorView.swift | 94 ++++++++++++--- .../Planning/MatchScheduleEditorView.swift | 31 ++++- .../Views/Planning/PlanningSettingsView.swift | 51 ++++---- .../Planning/RoundScheduleEditorView.swift | 39 +++++- PadelClub/Views/Player/PlayerDetailView.swift | 114 ++++++++++++++++++ PadelClub/Views/Round/LoserRoundsView.swift | 6 +- PadelClub/Views/Round/RoundView.swift | 4 +- .../Views/Shared/ImportedPlayerView.swift | 13 +- PadelClub/Views/Team/EditingTeamView.swift | 45 +++++++ PadelClub/Views/Team/TeamDetailView.swift | 4 +- .../TournamentFieldsManagerView.swift | 7 +- .../Screen/InscriptionManagerView.swift | 18 ++- .../Screen/TournamentCallView.swift | 44 +++++-- .../Screen/TournamentCashierView.swift | 33 ++++- .../Screen/TournamentScheduleView.swift | 5 + .../Screen/TournamentSettingsView.swift | 19 ++- .../Tournament/TournamentRunningView.swift | 2 + .../Views/Tournament/TournamentView.swift | 2 + .../ViewModifiers/DeferredViewModifier.swift | 13 ++ 33 files changed, 653 insertions(+), 145 deletions(-) create mode 100644 PadelClub/Views/Player/PlayerDetailView.swift create mode 100644 PadelClub/Views/Team/EditingTeamView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index ede2098..dba80b4 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -90,6 +90,8 @@ 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 */; }; + FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162842BD00279000C4809 /* PlayerDetailView.swift */; }; + FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.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 */; }; @@ -364,6 +366,8 @@ 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 = ""; }; + FF1162842BD00279000C4809 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = ""; }; + FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.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 = ""; }; @@ -724,6 +728,7 @@ isa = PBXGroup; children = ( FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */, + FF1162842BD00279000C4809 /* PlayerDetailView.swift */, FF089EB02BB001EA00F0AEC7 /* Components */, ); path = Player; @@ -1007,6 +1012,7 @@ FF967D082BAF3D4000A9A3BD /* TeamDetailView.swift */, FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */, FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */, + FF1162862BD004AD000C4809 /* EditingTeamView.swift */, ); path = Team; sourceTree = ""; @@ -1338,6 +1344,7 @@ FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */, FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */, FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */, + FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */, FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */, FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */, FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */, @@ -1371,6 +1378,7 @@ FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, FF967CF32BAECC0B00A9A3BD /* PlayerRegistration.swift in Sources */, FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */, + FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */, FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */, C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */, C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */, diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index edca863..723cc98 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -208,6 +208,10 @@ class GroupStage: ModelObject, Storable { Store.main.filter { $0.groupStage == self.id } } + func unsortedPlayers() -> [PlayerRegistration] { + unsortedTeams().flatMap({ $0.unsortedPlayers() }) + } + fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool fileprivate typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int) @@ -221,10 +225,13 @@ class GroupStage: ModelObject, Storable { } } + func unsortedTeams() -> [TeamRegistration] { + Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil } + } + func teams(_ sortedByScore: Bool = false) -> [TeamRegistration] { - let teams: [TeamRegistration] = Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil } if sortedByScore { - return teams.compactMap({ _score(forGroupStagePosition: $0.groupStagePosition!) }).sorted { (lhs, rhs) in + return unsortedTeams().compactMap({ _score(forGroupStagePosition: $0.groupStagePosition!) }).sorted { (lhs, rhs) in let predicates: [TeamScoreAreInIncreasingOrder] = [ { $0.wins < $1.wins }, { $0.setDifference < $1.setDifference }, @@ -244,7 +251,7 @@ class GroupStage: ModelObject, Storable { return false }.map({ $0.team }).reversed() } else { - return teams.sorted(by: \TeamRegistration.groupStagePosition!) + return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!) } } @@ -272,4 +279,8 @@ extension GroupStage: Selectable { func badgeValue() -> Int? { runningMatches().count } + + func badgeImage() -> String? { + hasEnded() ? "checkmark.circle.fill" : nil + } } diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 26e2224..e912c37 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -165,7 +165,11 @@ class PlayerRegistration: ModelObject, Storable { func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String { if let rank, rank > 0 { - return rank.formatted() + if rank != weight { + return weight.formatted() + " (" + rank.formatted() + ")" + } else { + return rank.formatted() + } } else { return "non classé" + (isMalePlayer() ? "" : "e") } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 37a4ab2..0380d30 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -418,4 +418,8 @@ extension Round: Selectable { return playedMatches().filter({ $0.isRunning() }).count } } + + func badgeImage() -> String? { + hasEnded() ? "checkmark.circle.fill" : nil + } } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index b0a9453..65bc7ed 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -285,7 +285,6 @@ class TeamRegistration: ModelObject, Storable { func initialRound() -> Round? { guard let bracketPosition else { return nil } - let matchIndex = RoundRule.matchIndex(fromBracketPosition: bracketPosition) let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2) return Store.main.filter(isIncluded: { $0.tournament == tournament && $0.index == roundIndex }).first } @@ -293,8 +292,7 @@ class TeamRegistration: ModelObject, Storable { func initialMatch() -> Match? { guard let bracketPosition else { return nil } guard let initialRoundObject = initialRound() else { return nil } - let matchIndex = RoundRule.matchIndex(fromBracketPosition: bracketPosition) - return Store.main.filter(isIncluded: { $0.round == initialRoundObject.id && $0.index == matchIndex }).first + return Store.main.filter(isIncluded: { $0.round == initialRoundObject.id && $0.index == bracketPosition / 2 }).first } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index e2be5d8..c57a020 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -528,6 +528,30 @@ class Tournament : ModelObject, Storable { } + func maximumCourtsPerGroupSage() -> Int { + if teamsPerGroupStage > 1 { + return min(teamsPerGroupStage / 2, courtCount) + } else { + return max(1, courtCount) + } + } + + func registrationIssues() -> Int { + let players : [PlayerRegistration] = unsortedPlayers() + let selectedTeams : [TeamRegistration] = selectedSortedTeams() + let callDateIssue : [TeamRegistration] = selectedTeams.filter { isStartDateIsDifferentThanCallDate($0) } + let duplicates : [PlayerRegistration] = duplicates(in: players) + let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == -1 }) + let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players) + let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players) + let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) + let waitingList : [TeamRegistration] = waitingListTeams(in: selectedTeams) + let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil }) + let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil }) + + return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + } + func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool { guard let callDate = team.callDate else { return false } if let groupStageStartDate = team.groupStageObject()?.startDate { diff --git a/PadelClub/ViewModel/AgendaDestination.swift b/PadelClub/ViewModel/AgendaDestination.swift index ad20c44..2f05f66 100644 --- a/PadelClub/ViewModel/AgendaDestination.swift +++ b/PadelClub/ViewModel/AgendaDestination.swift @@ -55,4 +55,8 @@ enum AgendaDestination: CaseIterable, Identifiable, Selectable { nil } } + + func badgeImage() -> String? { + nil + } } diff --git a/PadelClub/ViewModel/Selectable.swift b/PadelClub/ViewModel/Selectable.swift index 5836812..0f656cf 100644 --- a/PadelClub/ViewModel/Selectable.swift +++ b/PadelClub/ViewModel/Selectable.swift @@ -10,4 +10,5 @@ import Foundation protocol Selectable { func selectionLabel() -> String func badgeValue() -> Int? + func badgeImage() -> String? } diff --git a/PadelClub/Views/Calling/GroupStageCallingView.swift b/PadelClub/Views/Calling/GroupStageCallingView.swift index 7b003c6..1f8de11 100644 --- a/PadelClub/Views/Calling/GroupStageCallingView.swift +++ b/PadelClub/Views/Calling/GroupStageCallingView.swift @@ -48,16 +48,19 @@ struct GroupStageCallingView: View { groupStage.startDate } let keys = times.keys.compactMap { $0 }.sorted() - ForEach(keys, id: \.self) { key in - if let _groupStages = times[key], _groupStages.count > 1 { - let teams = _groupStages.flatMap { $0.teams() } - let callSeeds = teams.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) - Section { - CallView.CallStatusView(count: callSeeds.count, total: teams.count, startDate: key) - } header: { - Text(groupStages.map { $0.groupStageTitle() }.joined(separator: ", ")) - } footer: { - CallView(teams: teams, callDate: key, matchFormat: tournament.groupStageMatchFormat, roundLabel: "poule") + + if keys.count != groupStages.count { + ForEach(keys, id: \.self) { key in + if let _groupStages = times[key] { + let teams = _groupStages.flatMap { $0.teams() } + let callSeeds = teams.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) + Section { + CallView.CallStatusView(count: callSeeds.count, total: teams.count, startDate: key) + } header: { + Text(_groupStages.map { $0.groupStageTitle() }.joined(separator: ", ")) + } footer: { + CallView(teams: teams, callDate: key, matchFormat: tournament.groupStageMatchFormat, roundLabel: "poule") + } } } } diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 943aa37..517aaaf 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -45,9 +45,18 @@ struct GenericDestinationPickerView: View { } .buttonStyle(.plain) .overlay(alignment: .bottomTrailing) { - if let count = destination.badgeValue(), count > 0 { + if let image = destination.badgeImage() { + Image(systemName: image) + .foregroundColor(.green) + .imageScale(.medium) + .background ( + Color(.systemBackground) + .clipShape(.circle) + ) + .offset(x: 5, y: 5) + } else if let count = destination.badgeValue(), count > 0 { Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") - .foregroundColor(.secondary) + .foregroundColor(.red) .imageScale(.medium) .background ( Color(.systemBackground) diff --git a/PadelClub/Views/Components/RowButtonView.swift b/PadelClub/Views/Components/RowButtonView.swift index b8c3b34..bc7edc5 100644 --- a/PadelClub/Views/Components/RowButtonView.swift +++ b/PadelClub/Views/Components/RowButtonView.swift @@ -14,56 +14,73 @@ struct RowButtonView: View { let title: String var systemImage: String? = nil var image: String? = nil - var animatedProgress: Bool = false let confirmationMessage: String - let action: () -> () + var action: (() -> ())? = nil + var asyncAction: (() async -> ())? = nil + @State private var askConfirmation: Bool = false + @State private var isLoading = false - init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, animatedProgress: Bool = false, confirmationMessage: String? = nil, action: @escaping () -> Void) { + init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, confirmationMessage: String? = nil, action: @escaping (() -> ())) { self.role = role self.title = title self.systemImage = systemImage self.image = image - self.animatedProgress = animatedProgress self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage self.action = action } - + + init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, confirmationMessage: String? = nil, asyncAction: @escaping (() async -> ())) { + self.role = role + self.title = title + self.systemImage = systemImage + self.image = image + self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage + self.asyncAction = asyncAction + } + var body: some View { Button(role: role) { if role == .destructive { askConfirmation = true - } else { + } else if let action { action() + } else if let asyncAction { + isLoading = true + Task { + await asyncAction() + isLoading = false + } } } label: { HStack { - if animatedProgress { - Spacer() - ProgressView() - } else { - if let systemImage { - Image(systemName: systemImage) - .resizable() - .scaledToFit() - .frame(height: 24) - } - if let image { - Image(image) - .resizable() - .scaledToFit() - .frame(width: 32, height: 32) - } - Spacer() - Text(title) - .foregroundColor(.white) - .frame(height: 32) + if let systemImage { + Image(systemName: systemImage) + .resizable() + .scaledToFit() + .frame(height: 24) + } + if let image { + Image(image) + .resizable() + .scaledToFit() + .frame(width: 32, height: 32) } Spacer() + Text(title) + .opacity(isLoading ? 0.0 : 1.0) + .foregroundColor(.white) + .frame(height: 32) + Spacer() } .font(.headline) } - .disabled(animatedProgress) + .overlay { + if isLoading { + ProgressView() + } + } + .disabled(isLoading) .frame(maxWidth: .infinity) .buttonStyle(.borderedProminent) .tint(role == .destructive ? Color.red : Color.master) @@ -73,7 +90,15 @@ struct RowButtonView: View { isPresented: $askConfirmation, titleVisibility: .visible) { Button("OK") { - action() + if let action { + action() + } else if let asyncAction { + isLoading = true + Task { + await asyncAction() + isLoading = false + } + } } Button("Annuler", role: .cancel) {} } message: { diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index a5ea044..7dd0d07 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -41,6 +41,10 @@ struct GroupStagesView: View { return groupStage.badgeValue() } } + + func badgeImage() -> String? { + nil + } } init(tournament: Tournament) { diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index d61c9db..3e9384a 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -61,15 +61,7 @@ struct MainView: View { func _activityStatusBoxView() -> some View { _activityStatus() - .font(.title3) - .frame(height: 28) - .padding() - .background { - RoundedRectangle(cornerRadius: 20, style: .continuous) - .fill(.white) - } - .shadow(radius: 2) - .offset(y: -64) + .toastFormatted() } @ViewBuilder diff --git a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift index b2222a3..023360c 100644 --- a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift +++ b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift @@ -10,20 +10,54 @@ import SwiftUI struct GroupStageScheduleEditorView: View { @EnvironmentObject var dataStore: DataStore var groupStage: GroupStage + @State private var startDate: Date + @State private var dateUpdated: Bool = false + + + init(groupStage: GroupStage) { + self.groupStage = groupStage + self._startDate = State(wrappedValue: groupStage.startDate ?? Date()) + } var body: some View { @Bindable var groupStage = groupStage List { Section { MatchFormatPickerView(headerLabel: "Format", matchFormat: $groupStage.matchFormat) - } - - Section { - Text("Modifier l'horaire") - } - - RowButtonView("Convoquer") { - + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + .onChange(of: startDate) { + dateUpdated = true + } + } header: { + Text(groupStage.groupStageTitle()) + } footer: { + HStack { + Menu { + Text("à demain 9h") + Text("à la prochaine rotation") + Text("à la précédente rotation") + } label: { + Text("décaler") + .underline() + } + Spacer() + + if dateUpdated { + Button { + //todo, faut-il tout décaler ? + groupStage.startDate = startDate + _save() + dateUpdated = false + } label: { + Text("valider la modification") + .underline() + } + } + } + .font(.subheadline) + .buttonStyle(.borderless) } NavigationLink { @@ -35,10 +69,15 @@ struct GroupStageScheduleEditorView: View { .onChange(of: groupStage.matchFormat) { _save() } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } private func _save() { + let matches = groupStage._matches() + matches.forEach({ $0.matchFormat = groupStage.matchFormat }) + try? dataStore.matches.addOrUpdate(contentOfs: matches) try? dataStore.groupStages.addOrUpdate(instance: groupStage) } } diff --git a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift index 50fc9d6..b580ea7 100644 --- a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift @@ -15,7 +15,8 @@ struct LoserRoundStepScheduleEditorView: View { var matches: [Match] @State private var startDate: Date @State private var matchFormat: MatchFormat - + @State private var dateUpdated: Bool = false + init(round: Round, upperRound: Round) { self.upperRound = upperRound self.round = round @@ -30,16 +31,14 @@ struct LoserRoundStepScheduleEditorView: View { Section { MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday())) + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) } - - RowButtonView("Valider la modification") { - _updateSchedule() + .onChange(of: round.matchFormat) { + dateUpdated = true + } + .onChange(of: startDate) { + dateUpdated = true } - - } header: { - Text(round.selectionLabel()) - } footer: { NavigationLink { List { ForEach(matches) { match in @@ -52,8 +51,36 @@ struct LoserRoundStepScheduleEditorView: View { .navigationTitle(round.selectionLabel()) .environment(tournament) } label: { - Text("voir tous les matchs") + Text("Voir tous les matchs") + } + + } header: { + Text(round.selectionLabel()) + } footer: { + HStack { + Menu { + Text("à demain 9h") + Text("à la prochaine rotation") + Text("à la précédente rotation") + } label: { + Text("décaler") + .underline() + } + Spacer() + + if dateUpdated { + Button { + _updateSchedule() + dateUpdated = false + } label: { + Text("valider la modification") + .underline() + } + } } + .font(.subheadline) + .buttonStyle(.borderless) + } .headerProminence(.increased) } @@ -67,6 +94,9 @@ struct LoserRoundStepScheduleEditorView: View { _save() MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) + upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in + round.startDate = startDate + }) _save() } @@ -83,7 +113,8 @@ struct LoserRoundScheduleEditorView: View { var loserRounds: [Round] @State private var startDate: Date @State private var matchFormat: MatchFormat - + @State private var dateUpdated: Bool = false + init(upperRound: Round) { self.upperRound = upperRound let _loserRounds = upperRound.loserRounds() @@ -97,13 +128,34 @@ struct LoserRoundScheduleEditorView: View { Section { MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat) DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday())) - } - RowButtonView("Valider la modification") { - _updateSchedule() + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) } } header: { Text("Classement " + upperRound.roundTitle()) + } footer: { + HStack { + Menu { + Text("à demain 9h") + Text("à la prochaine rotation") + Text("à la précédente rotation") + } label: { + Text("décaler") + .underline() + } + Spacer() + + if dateUpdated { + Button { + _updateSchedule() + dateUpdated = false + } label: { + Text("valider la modification") + .underline() + } + } + } + .font(.subheadline) + .buttonStyle(.borderless) } @@ -113,6 +165,12 @@ struct LoserRoundScheduleEditorView: View { } } } + .onChange(of: startDate) { + dateUpdated = true + } + .onChange(of: matchFormat) { + dateUpdated = true + } .headerProminence(.increased) .navigationTitle("Réglages") .toolbarBackground(.visible, for: .navigationBar) @@ -126,12 +184,14 @@ struct LoserRoundScheduleEditorView: View { upperRound.loserRounds().forEach({ round in round.resetRound(updateMatchFormat: matchFormat) }) - + try? dataStore.matches.addOrUpdate(contentOfs: matches) _save() - + MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: upperRound.loserRounds().first?.id, fromMatchId: nil, startDate: startDate) _save() + + upperRound.loserRounds().first?.startDate = startDate } private func _save() { diff --git a/PadelClub/Views/Planning/MatchScheduleEditorView.swift b/PadelClub/Views/Planning/MatchScheduleEditorView.swift index b1af2c8..b5b39cb 100644 --- a/PadelClub/Views/Planning/MatchScheduleEditorView.swift +++ b/PadelClub/Views/Planning/MatchScheduleEditorView.swift @@ -11,6 +11,7 @@ struct MatchScheduleEditorView: View { @Environment(Tournament.self) var tournament: Tournament var match: Match @State private var startDate: Date + @State private var dateUpdated: Bool = false init(match: Match) { self.match = match @@ -20,10 +21,10 @@ struct MatchScheduleEditorView: View { var body: some View { Section { DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday())) + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) } - RowButtonView("Valider la modification") { - _updateSchedule() + .onChange(of: startDate) { + dateUpdated = true } } header: { if let round = match.roundObject { @@ -31,6 +32,30 @@ struct MatchScheduleEditorView: View { } else { Text(match.matchTitle()) } + } footer: { + HStack { + Menu { + Text("à demain 9h") + Text("à la prochaine rotation") + Text("à la précédente rotation") + } label: { + Text("décaler") + .underline() + } + Spacer() + + if dateUpdated { + Button { + _updateSchedule() + dateUpdated = false + } label: { + Text("valider la modification") + .underline() + } + } + } + .font(.subheadline) + .buttonStyle(.borderless) } .headerProminence(.increased) } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 0721e80..601bb67 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -10,7 +10,6 @@ import SwiftUI struct PlanningSettingsView: View { @EnvironmentObject var dataStore: DataStore var tournament: Tournament - @State private var scheduleSetup: Bool = false @State private var randomCourtDistribution: Bool @State private var groupStageCourtCount: Int @State private var upperBracketBreakTime: Bool @@ -20,6 +19,8 @@ struct PlanningSettingsView: View { @State private var upperBracketRotationDifference: Int @State private var timeDifferenceLimit: Double @State private var shouldHandleUpperRoundSlice: Bool + @State private var isScheduling: Bool = false + @State private var schedulingDone: Bool = false init(tournament: Tournament) { self.tournament = tournament @@ -52,10 +53,10 @@ struct PlanningSettingsView: View { } Section { - TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) + TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount, max: 100) if tournament.groupStages().isEmpty == false { - TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount) + TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount, max: tournament.maximumCourtsPerGroupSage()) } NavigationLink { @@ -102,18 +103,36 @@ struct PlanningSettingsView: View { .disabled(rotationDifferenceIsImportant == false) //timeDifferenceLimit - RowButtonView("Horaire intelligent", role: .destructive) { - _setupSchedule() + schedulingDone = false + await _setupSchedule() + schedulingDone = true } - - if scheduleSetup { - HStack { - Image(systemName: "checkmark") - } + } + + Section { + RowButtonView("Supprimer tous les horaires", role: .destructive) { + let allMatches = tournament.allMatches() + allMatches.forEach({ $0.startDate = nil }) + try? dataStore.matches.addOrUpdate(contentOfs: allMatches) + + let allGroupStages = tournament.groupStages() + allGroupStages.forEach({ $0.startDate = nil }) + try? dataStore.groupStages.addOrUpdate(contentOfs: allGroupStages) + + let allRounds = tournament.allRounds() + allRounds.forEach({ $0.startDate = nil }) + try? dataStore.rounds.addOrUpdate(contentOfs: allRounds) } } } + .overlay(alignment: .bottom) { + if schedulingDone { + Label("Horaires mis à jour", systemImage: "checkmark.circle.fill") + .toastFormatted() + .deferredRendering(for: .seconds(2)) + } + } .onChange(of: groupStageCourtCount) { tournament.groupStageCourtCount = groupStageCourtCount _save() @@ -132,7 +151,7 @@ struct PlanningSettingsView: View { } } - private func _setupSchedule() { + private func _setupSchedule() async { let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 let groupStages = tournament.groupStages() let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount @@ -166,13 +185,6 @@ struct PlanningSettingsView: View { let matches = tournament.groupStages().flatMap({ $0._matches() }) matches.forEach({ $0.startDate = nil }) -// var times = Set(groupStages.compactMap { $0.startDate }.filter { $0 >= tournament.startDate } ) -// if times.isEmpty { -// groupStages.forEach({ $0.startDate = tournament.startDate }) -// times.insert(tournament.startDate) -// try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages) -// } - var lastDate : Date = tournament.startDate groupStages.chunked(into: groupStageCourtCount).forEach { groups in groups.forEach({ $0.startDate = lastDate }) @@ -195,9 +207,6 @@ struct PlanningSettingsView: View { try? dataStore.matches.addOrUpdate(contentOfs: matches) matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate) - - scheduleSetup = true - } private func _save() { diff --git a/PadelClub/Views/Planning/RoundScheduleEditorView.swift b/PadelClub/Views/Planning/RoundScheduleEditorView.swift index d16ba98..5ced1eb 100644 --- a/PadelClub/Views/Planning/RoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/RoundScheduleEditorView.swift @@ -13,7 +13,8 @@ struct RoundScheduleEditorView: View { var round: Round @State private var startDate: Date - + @State private var dateUpdated: Bool = false + init(round: Round) { self.round = round self._startDate = State(wrappedValue: round.startDate ?? round.playedMatches().first?.startDate ?? Date()) @@ -25,17 +26,46 @@ struct RoundScheduleEditorView: View { Section { MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday())) + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + .onChange(of: round.matchFormat) { + dateUpdated = true + } + .onChange(of: startDate) { + dateUpdated = true } - RowButtonView("Valider la modification") { - _updateSchedule() + } footer: { + HStack { + Menu { + Text("à demain 9h") + Text("à la prochaine rotation") + Text("à la précédente rotation") + } label: { + Text("décaler") + .underline() + } + Spacer() + + if dateUpdated { + Button { + _updateSchedule() + dateUpdated = false + } label: { + Text("valider la modification") + .underline() + } + } } + .font(.subheadline) + .buttonStyle(.borderless) } ForEach(round.playedMatches()) { match in MatchScheduleEditorView(match: match) } } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } private func _updateSchedule() { @@ -47,6 +77,7 @@ struct RoundScheduleEditorView: View { _save() MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) + round.startDate = startDate _save() } diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift new file mode 100644 index 0000000..6b0624a --- /dev/null +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -0,0 +1,114 @@ +// +// PlayerDetailView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct PlayerDetailView: View { + @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore + @Bindable var player: PlayerRegistration + @FocusState private var textFieldIsFocus: Bool + + var body: some View { + Form { + Section { + LabeledContent { + TextField("Nom", text: $player.lastName) + .keyboardType(.alphabet) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + } label: { + Text("Nom") + } + + LabeledContent { + TextField("Prénom", text: $player.firstName) + .keyboardType(.alphabet) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + } label: { + Text("Prénom") + } + + PlayerSexPickerView(player: player) + } + + Section { + LabeledContent { + TextField("Rang", value: $player.rank, format: .number) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + .focused($textFieldIsFocus) + } label: { + Text("Rang") + } + } header: { + Text("Classement actuel") + } + + if player.isMalePlayer() == false && tournament.tournamentCategory == .men, let rank = player.rank { + Section { + let value = PlayerRegistration.addon(for: rank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0) + LabeledContent { + Text(value.formatted()) + } label: { + Text("Valeur à rajouter") + } + LabeledContent { + TextField("Rang", value: $player.weight, format: .number) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + .focused($textFieldIsFocus) + } label: { + Text("Poids re-calculé") + } + } header: { + Text("Ré-assimilation") + } footer: { + Text("Calculé en fonction du sexe") + } + } + } + .scrollDismissesKeyboard(.immediately) + .onChange(of: player.sex) { + _save() + } + .onChange(of: player.weight) { + player.team()?.updateWeight() + _save() + } + .onChange(of: player.rank) { + player.setWeight(in: tournament) + player.team()?.updateWeight() + _save() + } + .headerProminence(.increased) + .navigationTitle("Édition") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .keyboard) { + Button("Valider") { + textFieldIsFocus = false + } + } + } + } + + private func _save() { + try? dataStore.playerRegistrations.addOrUpdate(instance: player) + if let team = player.team() { + try? dataStore.teamRegistrations.addOrUpdate(instance: team) + } + } +} + +#Preview { + PlayerDetailView(player: PlayerRegistration.mock()) +} diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 9c86850..8a311a3 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -73,8 +73,10 @@ struct LoserRoundView: View { } .headerProminence(.increased) .toolbar { - Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { - isEditingTournamentSeed.wrappedValue.toggle() + ToolbarItem(placement: .topBarTrailing) { + Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { + isEditingTournamentSeed.wrappedValue.toggle() + } } } } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index ffa25a3..c9e0ab2 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -53,7 +53,9 @@ struct RoundView: View { .headerProminence(.increased) .toolbar { ToolbarItem(placement: .topBarTrailing) { - EditButton() + Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { + isEditingTournamentSeed.wrappedValue.toggle() + } } } } diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index e010b79..392c64a 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -29,9 +29,6 @@ struct ImportedPlayerView: View { .foregroundStyle(.secondary) .font(.caption) } - } else if let computedAge = player.computedAge { - Text(computedAge.formatted() + " ans") - .foregroundStyle(.secondary) } } .font(.title3) @@ -61,9 +58,13 @@ struct ImportedPlayerView: View { } } - Text(player.formattedLicense()) - .font(.caption) - + HStack { + Text(player.formattedLicense()) + if let computedAge = player.computedAge { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) if let clubName = player.clubName { Text(clubName) .font(.caption) diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift new file mode 100644 index 0000000..db408eb --- /dev/null +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -0,0 +1,45 @@ +// +// EditingTeamView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct EditingTeamView: View { + @EnvironmentObject var dataStore: DataStore + var team: TeamRegistration + @State private var registrationDate : Date + + init(team: TeamRegistration) { + self.team = team + _registrationDate = State(wrappedValue: team.registrationDate ?? Date()) + } + + var body: some View { + List { + Section { + DatePicker(registrationDate.formatted(.dateTime.weekday()), selection: $registrationDate) + } header: { + Text("Date d'inscription") + } + } + .onChange(of: registrationDate) { + team.registrationDate = registrationDate + _save() + } + .headerProminence(.increased) + .navigationTitle("Édition") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } + + private func _save() { + try? dataStore.teamRegistrations.addOrUpdate(instance: team) + } +} + +#Preview { + EditingTeamView(team: TeamRegistration.mock()) +} diff --git a/PadelClub/Views/Team/TeamDetailView.swift b/PadelClub/Views/Team/TeamDetailView.swift index cff5a51..8325fc2 100644 --- a/PadelClub/Views/Team/TeamDetailView.swift +++ b/PadelClub/Views/Team/TeamDetailView.swift @@ -8,6 +8,7 @@ import SwiftUI struct TeamDetailView: View { + @Environment(Tournament.self) var tournament: Tournament @EnvironmentObject var dataStore: DataStore var team: TeamRegistration @@ -17,7 +18,8 @@ struct TeamDetailView: View { } else { ForEach(team.players()) { player in NavigationLink { - Text("Hello wolrd") + PlayerDetailView(player: player) + .environment(tournament) } label: { PlayerView(player: player) } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift index 56bf2d7..5ada9bf 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift @@ -10,18 +10,19 @@ import SwiftUI struct TournamentFieldsManagerView: View { let localizedStringKey: String @Binding var count: Int + let max: Int var body: some View { LabeledContent { - StepperView(count: $count, minimum: 1, maximum: 1_000) + StepperView(count: $count, minimum: 1, maximum: max) } label: { Text(localizedStringKey) - Text(count.formatted()) +// Text(count.formatted()) } } } #Preview { - TournamentFieldsManagerView(localizedStringKey: "test", count: .constant(2)) + TournamentFieldsManagerView(localizedStringKey: "test", count: .constant(2), max: 10) .environment(Tournament.mock()) } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 041f9ec..dd6a40b 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -418,14 +418,16 @@ struct InscriptionManagerView: View { .environment(tournament) } label: { LabeledContent { - Text(count.formatted() + "/" + tournament.teamCount.formatted()) + Text(tournament.registrationIssues().formatted()).font(.largeTitle) } label: { - Text("Analyse des inscriptions") + Text("Problèmes détéctés") if let closedRegistrationDate = tournament.closedRegistrationDate { Text("clôturé le " + closedRegistrationDate.formatted()) } } } + } header: { + Text(count.formatted() + "/" + tournament.teamCount.formatted() + " paires inscrites") } } @@ -739,7 +741,7 @@ struct InscriptionManagerView: View { private func _teamFooterView(_ team: TeamRegistration) -> some View { HStack { if let formattedRegistrationDate = team.formattedInscriptionDate() { - Text(formattedRegistrationDate).foregroundStyle(.secondary) + Text(formattedRegistrationDate) } Spacer() _teamMenuOptionView(team) @@ -748,7 +750,7 @@ struct InscriptionManagerView: View { private func _teamMenuOptionView(_ team: TeamRegistration) -> some View { Menu { Section { - Button("Modifier l'équipe") { + Button("Changer les joueurs") { editedTeam = team team.unsortedPlayers().forEach { player in createdPlayers.insert(player) @@ -756,7 +758,13 @@ struct InscriptionManagerView: View { } } Divider() - + NavigationLink { + EditingTeamView(team: team) + .environment(tournament) + } label: { + Text("Éditer une donnée de l'équipe") + } + Divider() Toggle(isOn: .init(get: { return team.wildCardBracket }, set: { value in diff --git a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift index 3f10f50..af1e0d3 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift @@ -7,11 +7,18 @@ import SwiftUI -enum CallDestination: String, Identifiable, Selectable { - case seeds - case groupStages +enum CallDestination: Identifiable, Selectable { + case seeds(Tournament) + case groupStages(Tournament) - var id: String { self.rawValue } + var id: String { + switch self { + case .seeds: + return "seed" + case .groupStages: + return "groupStage" + } + } func selectionLabel() -> String { switch self { @@ -23,8 +30,27 @@ enum CallDestination: String, Identifiable, Selectable { } func badgeValue() -> Int? { - nil + switch self { + case .seeds(let tournament): + let allSeedCalled = tournament.seeds().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) + return allSeedCalled.count + case .groupStages(let tournament): + let allSeedCalled = tournament.groupStageTeams().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) + return allSeedCalled.count + } + } + + func badgeImage() -> String? { + switch self { + case .seeds(let tournament): + let allSeedCalled = tournament.seeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) + return allSeedCalled ? "checkmark.circle.fill" : nil + case .groupStages(let tournament): + let allSeedCalled = tournament.groupStageTeams().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) + return allSeedCalled ? "checkmark.circle.fill" : nil + } } + } @@ -38,13 +64,13 @@ struct TournamentCallView: View { var destinations = [CallDestination]() let groupStageTeams = tournament.groupStageTeams() if groupStageTeams.isEmpty == false { - destinations.append(.groupStages) - self._selectedDestination = State(wrappedValue: .groupStages) + destinations.append(.groupStages(tournament)) + self._selectedDestination = State(wrappedValue: .groupStages(tournament)) } if tournament.seededTeams().isEmpty == false { - destinations.append(.seeds) + destinations.append(.seeds(tournament)) if groupStageTeams.isEmpty { - self._selectedDestination = State(wrappedValue: .seeds) + self._selectedDestination = State(wrappedValue: .seeds(tournament)) } } self.allDestinations = destinations diff --git a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift index b6a6445..9f1730d 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift @@ -11,7 +11,7 @@ enum CashierDestination: Identifiable, Selectable { case summary case groupStage(GroupStage) case bracket(Round) - case all + case all(Tournament) var id: String { switch self { @@ -38,8 +38,31 @@ enum CashierDestination: Identifiable, Selectable { } func badgeValue() -> Int? { - nil + switch self { + case .summary: + return nil + case .groupStage(let groupStage): + return groupStage.unsortedPlayers().filter({ $0.hasPaid() == false }).count + case .bracket(let round): + return round.seeds().flatMap { $0.unsortedPlayers() }.filter({ $0.hasPaid() == false }).count + case .all(let tournament): + return tournament.selectedPlayers().filter({ $0.hasPaid() == false }).count + } } + + func badgeImage() -> String? { + switch self { + case .summary: + return nil + case .groupStage(let groupStage): + return groupStage.unsortedPlayers().allSatisfy({ $0.hasPaid() }) ? "checkmark" : nil + case .bracket(let round): + return round.seeds().flatMap { $0.unsortedPlayers() }.allSatisfy({ $0.hasPaid() }) ? "checkmark" : nil + case .all(let tournament): + return tournament.selectedPlayers().allSatisfy({ $0.hasPaid() }) ? "checkmark" : nil + } + } + } struct TournamentCashierView: View { @@ -47,7 +70,7 @@ struct TournamentCashierView: View { @State private var selectedDestination: CashierDestination? func allDestinations() -> [CashierDestination] { - var allDestinations : [CashierDestination] = [.summary, .all] + var allDestinations : [CashierDestination] = [] let destinations : [CashierDestination] = tournament.groupStages().map { CashierDestination.groupStage($0) } allDestinations.append(contentsOf: destinations) tournament.rounds().forEach { round in @@ -55,6 +78,8 @@ struct TournamentCashierView: View { allDestinations.append(CashierDestination.bracket(round)) } } + allDestinations.append(.all(tournament)) + allDestinations.append(.summary) return allDestinations } @@ -83,7 +108,7 @@ struct TournamentCashierView: View { CashierView(tournament: tournament, teams: groupStage.teams()) case .bracket(let round): CashierView(tournament: tournament, teams: round.seeds()) - case .all: + case .all(let tournament): CashierView(tournament: tournament, teams: tournament.selectedSortedTeams()) } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift index 93b3881..fccb5a2 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift @@ -41,6 +41,11 @@ enum ScheduleDestination: String, Identifiable, Selectable { func badgeValue() -> Int? { nil } + + func badgeImage() -> String? { + nil + } + } struct TournamentScheduleView: View { diff --git a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift index c6c6500..b05d84b 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift @@ -12,15 +12,16 @@ struct TournamentSettingsView: View { @EnvironmentObject var dataStore: DataStore @State private var tournamentName: String = "" - + @FocusState private var textFieldIsFocus: Bool + var body: some View { @Bindable var tournament = tournament Form { LabeledContent { TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $tournament.entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) .keyboardType(.decimalPad) - .fixedSize() .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) } label: { Text("Inscription") } @@ -28,7 +29,7 @@ struct TournamentSettingsView: View { LabeledContent { TextField("Nom", text: $tournamentName) .multilineTextAlignment(.trailing) - .fixedSize() + .frame(maxWidth: .infinity) .keyboardType(.alphabet) .autocorrectionDisabled() .onSubmit { @@ -44,7 +45,7 @@ struct TournamentSettingsView: View { TournamentLevelPickerView() TournamentDurationManagerView() - TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) + TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount, max: 100) TournamentDatePickerView() @@ -83,7 +84,6 @@ struct TournamentSettingsView: View { event.club = nil try? dataStore.events.addOrUpdate(instance: event) } - .font(.caption) } } } @@ -91,8 +91,17 @@ struct TournamentSettingsView: View { TournamentFormatSelectionView() } + .focused($textFieldIsFocus) + .scrollDismissesKeyboard(.immediately) .navigationTitle("Réglages") .toolbarBackground(.visible, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .keyboard) { + Button("Valider") { + textFieldIsFocus = false + } + } + } .onDisappear { try? dataStore.tournaments.addOrUpdate(instance: tournament) } diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index 3d1194c..2bd172b 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -49,6 +49,7 @@ struct TournamentRunningView: View { NavigationLink(value: Screen.groupStage) { LabeledContent { Text(tournament.groupStageStatus()) + .foregroundStyle(.master) } label: { Text("Poules") } @@ -60,6 +61,7 @@ struct TournamentRunningView: View { NavigationLink(value: Screen.round) { LabeledContent { Text(tournament.bracketStatus()) + .foregroundStyle(.master) } label: { Text("Tableau") } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index c27cfde..c4cfda0 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -34,6 +34,7 @@ struct TournamentView: View { NavigationLink(value: Screen.inscription) { LabeledContent { Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted()) + .foregroundStyle(.master) } label: { Text("Gestion des inscriptions") if let closedRegistrationDate = tournament.closedRegistrationDate { @@ -44,6 +45,7 @@ struct TournamentView: View { if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false { LabeledContent { Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened)) + .foregroundStyle(.master) } label: { Text("Date limite") } diff --git a/PadelClub/Views/ViewModifiers/DeferredViewModifier.swift b/PadelClub/Views/ViewModifiers/DeferredViewModifier.swift index 98c4591..6a2976d 100644 --- a/PadelClub/Views/ViewModifiers/DeferredViewModifier.swift +++ b/PadelClub/Views/ViewModifiers/DeferredViewModifier.swift @@ -49,6 +49,19 @@ private struct DeferredViewModifier: ViewModifier { } extension View { + func toastFormatted() -> some View { + self + .font(.title3) + .frame(height: 28) + .padding() + .background { + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(.white) + } + .shadow(radius: 2) + .offset(y: -64) + } + func deferredRendering(for delay: DispatchTimeInterval) -> some View { modifier(DeferredViewModifier(delay: delay)) } From 6d2893035940ad5ef5845c66e40ea3b163f591e4 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 18 Apr 2024 04:32:28 +0200 Subject: [PATCH 05/11] add date manager view component --- PadelClub.xcodeproj/project.pbxproj | 16 +++ .../GenericDestinationPickerView.swift | 4 +- .../Components/DateUpdateManagerView.swift | 54 ++++++++ .../GroupStageScheduleEditorView.swift | 27 +--- .../LoserRoundScheduleEditorView.swift | 129 +----------------- .../LoserRoundStepScheduleEditorView.swift | 79 +++++++++++ .../Planning/MatchScheduleEditorView.swift | 28 +--- .../Planning/RoundScheduleEditorView.swift | 31 +---- .../Screen/TournamentCashierView.swift | 6 +- 9 files changed, 163 insertions(+), 211 deletions(-) create mode 100644 PadelClub/Views/Planning/Components/DateUpdateManagerView.swift create mode 100644 PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index dba80b4..7debfe4 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -92,6 +92,8 @@ FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */; }; FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162842BD00279000C4809 /* PlayerDetailView.swift */; }; FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; }; + FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; + FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.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 */; }; @@ -368,6 +370,8 @@ FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditablePlayerView.swift; sourceTree = ""; }; FF1162842BD00279000C4809 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = ""; }; FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = ""; }; + FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = ""; }; + FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.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 = ""; }; @@ -794,6 +798,14 @@ path = Cashier; sourceTree = ""; }; + FF1162882BD0523B000C4809 /* Components */ = { + isa = PBXGroup; + children = ( + FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */, + ); + path = Components; + sourceTree = ""; + }; FF1DC54D2BAB34FA00FD8220 /* Club */ = { isa = PBXGroup; children = ( @@ -1107,8 +1119,10 @@ FFF964542BC266CF00EEF017 /* SchedulerView.swift */, FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */, FFFCDE0D2BCC833600317DEF /* LoserRoundScheduleEditorView.swift */, + FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */, FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */, FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */, + FF1162882BD0523B000C4809 /* Components */, ); path = Planning; sourceTree = ""; @@ -1320,6 +1334,7 @@ FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */, FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */, FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */, + FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF089EBF2BB0B14600F0AEC7 /* FileImportView.swift in Sources */, C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */, FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */, @@ -1429,6 +1444,7 @@ FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, + FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */, FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */, FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */, FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 517aaaf..13c0adf 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -53,7 +53,7 @@ struct GenericDestinationPickerView: View { Color(.systemBackground) .clipShape(.circle) ) - .offset(x: 5, y: 5) + .offset(x: 3, y: 3) } else if let count = destination.badgeValue(), count > 0 { Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") .foregroundColor(.red) @@ -62,7 +62,7 @@ struct GenericDestinationPickerView: View { Color(.systemBackground) .clipShape(.circle) ) - .offset(x: 5, y: 5) + .offset(x: 3, y: 3) } } } diff --git a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift new file mode 100644 index 0000000..1c30303 --- /dev/null +++ b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift @@ -0,0 +1,54 @@ +// +// DateUpdateManagerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +enum DateUpdate { + case nextRotation + case previousRotation + case tomorrowAtNine + case inMinutes(Int) + case afterRound(Round) + case afterGroupStage(GroupStage) +} + +struct DateUpdateManagerView: View { + @Binding var startDate: Date + @State private var dateUpdated: Bool = false + + var validateAction: () -> Void + + var body: some View { + HStack { + Menu { + Text("à demain 9h") + Text("à la prochaine rotation") + Text("à la précédente rotation") + } label: { + Text("décaler") + .underline() + } + Spacer() + + if dateUpdated { + Button { + validateAction() + dateUpdated = false + } label: { + Text("valider la modification") + .underline() + } + } + } + .font(.subheadline) + .buttonStyle(.borderless) + .onChange(of: startDate) { + dateUpdated = true + } + } +} + diff --git a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift index 023360c..db8860a 100644 --- a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift +++ b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift @@ -33,31 +33,10 @@ struct GroupStageScheduleEditorView: View { } header: { Text(groupStage.groupStageTitle()) } footer: { - HStack { - Menu { - Text("à demain 9h") - Text("à la prochaine rotation") - Text("à la précédente rotation") - } label: { - Text("décaler") - .underline() - } - Spacer() - - if dateUpdated { - Button { - //todo, faut-il tout décaler ? - groupStage.startDate = startDate - _save() - dateUpdated = false - } label: { - Text("valider la modification") - .underline() - } - } + DateUpdateManagerView(startDate: $startDate) { + groupStage.startDate = startDate + _save() } - .font(.subheadline) - .buttonStyle(.borderless) } NavigationLink { diff --git a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift index b580ea7..5b58990 100644 --- a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift @@ -6,104 +6,6 @@ // import SwiftUI -struct LoserRoundStepScheduleEditorView: View { - @EnvironmentObject var dataStore: DataStore - @Environment(Tournament.self) var tournament: Tournament - - var round: Round - var upperRound: Round - var matches: [Match] - @State private var startDate: Date - @State private var matchFormat: MatchFormat - @State private var dateUpdated: Bool = false - - init(round: Round, upperRound: Round) { - self.upperRound = upperRound - self.round = round - let _matches = upperRound.loserRounds(forRoundIndex: round.index).flatMap({ $0.playedMatches() }) - self.matches = _matches - self._startDate = State(wrappedValue: round.startDate ?? _matches.first?.startDate ?? Date()) - self._matchFormat = State(wrappedValue: round.matchFormat) - } - - var body: some View { - @Bindable var round = round - Section { - MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) - DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) - } - .onChange(of: round.matchFormat) { - dateUpdated = true - } - .onChange(of: startDate) { - dateUpdated = true - } - NavigationLink { - List { - ForEach(matches) { match in - if match.disabled == false { - MatchScheduleEditorView(match: match) - } - } - } - .headerProminence(.increased) - .navigationTitle(round.selectionLabel()) - .environment(tournament) - } label: { - Text("Voir tous les matchs") - } - - } header: { - Text(round.selectionLabel()) - } footer: { - HStack { - Menu { - Text("à demain 9h") - Text("à la prochaine rotation") - Text("à la précédente rotation") - } label: { - Text("décaler") - .underline() - } - Spacer() - - if dateUpdated { - Button { - _updateSchedule() - dateUpdated = false - } label: { - Text("valider la modification") - .underline() - } - } - } - .font(.subheadline) - .buttonStyle(.borderless) - - } - .headerProminence(.increased) - } - - private func _updateSchedule() { - upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in - round.resetRound(updateMatchFormat: round.matchFormat) - }) - - try? dataStore.matches.addOrUpdate(contentOfs: matches) - _save() - - MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) - upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in - round.startDate = startDate - }) - _save() - } - - private func _save() { - try? dataStore.rounds.addOrUpdate(contentOfs: upperRound.loserRounds(forRoundIndex: round.index)) - } -} struct LoserRoundScheduleEditorView: View { @EnvironmentObject var dataStore: DataStore @@ -113,7 +15,6 @@ struct LoserRoundScheduleEditorView: View { var loserRounds: [Round] @State private var startDate: Date @State private var matchFormat: MatchFormat - @State private var dateUpdated: Bool = false init(upperRound: Round) { self.upperRound = upperRound @@ -133,29 +34,9 @@ struct LoserRoundScheduleEditorView: View { } header: { Text("Classement " + upperRound.roundTitle()) } footer: { - HStack { - Menu { - Text("à demain 9h") - Text("à la prochaine rotation") - Text("à la précédente rotation") - } label: { - Text("décaler") - .underline() - } - Spacer() - - if dateUpdated { - Button { - _updateSchedule() - dateUpdated = false - } label: { - Text("valider la modification") - .underline() - } - } + DateUpdateManagerView(startDate: $startDate) { + _updateSchedule() } - .font(.subheadline) - .buttonStyle(.borderless) } @@ -165,12 +46,6 @@ struct LoserRoundScheduleEditorView: View { } } } - .onChange(of: startDate) { - dateUpdated = true - } - .onChange(of: matchFormat) { - dateUpdated = true - } .headerProminence(.increased) .navigationTitle("Réglages") .toolbarBackground(.visible, for: .navigationBar) diff --git a/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift new file mode 100644 index 0000000..f4f48dd --- /dev/null +++ b/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift @@ -0,0 +1,79 @@ +// +// LoserRoundStepScheduleEditorView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct LoserRoundStepScheduleEditorView: View { + @EnvironmentObject var dataStore: DataStore + @Environment(Tournament.self) var tournament: Tournament + + var round: Round + var upperRound: Round + var matches: [Match] + @State private var startDate: Date + @State private var matchFormat: MatchFormat + + init(round: Round, upperRound: Round) { + self.upperRound = upperRound + self.round = round + let _matches = upperRound.loserRounds(forRoundIndex: round.index).flatMap({ $0.playedMatches() }) + self.matches = _matches + self._startDate = State(wrappedValue: round.startDate ?? _matches.first?.startDate ?? Date()) + self._matchFormat = State(wrappedValue: round.matchFormat) + } + + var body: some View { + @Bindable var round = round + Section { + MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + NavigationLink { + List { + ForEach(matches) { match in + if match.disabled == false { + MatchScheduleEditorView(match: match) + } + } + } + .headerProminence(.increased) + .navigationTitle(round.selectionLabel()) + .environment(tournament) + } label: { + Text("Voir tous les matchs") + } + + } header: { + Text(round.selectionLabel()) + } footer: { + DateUpdateManagerView(startDate: $startDate) { + _updateSchedule() + } + } + .headerProminence(.increased) + } + + private func _updateSchedule() { + upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in + round.resetRound(updateMatchFormat: round.matchFormat) + }) + + try? dataStore.matches.addOrUpdate(contentOfs: matches) + _save() + + MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) + upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in + round.startDate = startDate + }) + _save() + } + + private func _save() { + try? dataStore.rounds.addOrUpdate(contentOfs: upperRound.loserRounds(forRoundIndex: round.index)) + } +} diff --git a/PadelClub/Views/Planning/MatchScheduleEditorView.swift b/PadelClub/Views/Planning/MatchScheduleEditorView.swift index b5b39cb..2f24b72 100644 --- a/PadelClub/Views/Planning/MatchScheduleEditorView.swift +++ b/PadelClub/Views/Planning/MatchScheduleEditorView.swift @@ -11,7 +11,6 @@ struct MatchScheduleEditorView: View { @Environment(Tournament.self) var tournament: Tournament var match: Match @State private var startDate: Date - @State private var dateUpdated: Bool = false init(match: Match) { self.match = match @@ -23,9 +22,6 @@ struct MatchScheduleEditorView: View { DatePicker(selection: $startDate) { Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) } - .onChange(of: startDate) { - dateUpdated = true - } } header: { if let round = match.roundObject { Text(round.roundTitle() + " " + match.matchTitle()) @@ -33,29 +29,9 @@ struct MatchScheduleEditorView: View { Text(match.matchTitle()) } } footer: { - HStack { - Menu { - Text("à demain 9h") - Text("à la prochaine rotation") - Text("à la précédente rotation") - } label: { - Text("décaler") - .underline() - } - Spacer() - - if dateUpdated { - Button { - _updateSchedule() - dateUpdated = false - } label: { - Text("valider la modification") - .underline() - } - } + DateUpdateManagerView(startDate: $startDate) { + _updateSchedule() } - .font(.subheadline) - .buttonStyle(.borderless) } .headerProminence(.increased) } diff --git a/PadelClub/Views/Planning/RoundScheduleEditorView.swift b/PadelClub/Views/Planning/RoundScheduleEditorView.swift index 5ced1eb..41640dc 100644 --- a/PadelClub/Views/Planning/RoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/RoundScheduleEditorView.swift @@ -13,7 +13,6 @@ struct RoundScheduleEditorView: View { var round: Round @State private var startDate: Date - @State private var dateUpdated: Bool = false init(round: Round) { self.round = round @@ -28,36 +27,10 @@ struct RoundScheduleEditorView: View { DatePicker(selection: $startDate) { Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) } - .onChange(of: round.matchFormat) { - dateUpdated = true - } - .onChange(of: startDate) { - dateUpdated = true - } } footer: { - HStack { - Menu { - Text("à demain 9h") - Text("à la prochaine rotation") - Text("à la précédente rotation") - } label: { - Text("décaler") - .underline() - } - Spacer() - - if dateUpdated { - Button { - _updateSchedule() - dateUpdated = false - } label: { - Text("valider la modification") - .underline() - } - } + DateUpdateManagerView(startDate: $startDate) { + _updateSchedule() } - .font(.subheadline) - .buttonStyle(.borderless) } ForEach(round.playedMatches()) { match in diff --git a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift index 9f1730d..1de11e8 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift @@ -55,11 +55,11 @@ enum CashierDestination: Identifiable, Selectable { case .summary: return nil case .groupStage(let groupStage): - return groupStage.unsortedPlayers().allSatisfy({ $0.hasPaid() }) ? "checkmark" : nil + return groupStage.unsortedPlayers().allSatisfy({ $0.hasPaid() }) ? "checkmark.circle.fill" : nil case .bracket(let round): - return round.seeds().flatMap { $0.unsortedPlayers() }.allSatisfy({ $0.hasPaid() }) ? "checkmark" : nil + return round.seeds().flatMap { $0.unsortedPlayers() }.allSatisfy({ $0.hasPaid() }) ? "checkmark.circle.fill" : nil case .all(let tournament): - return tournament.selectedPlayers().allSatisfy({ $0.hasPaid() }) ? "checkmark" : nil + return tournament.selectedPlayers().allSatisfy({ $0.hasPaid() }) ? "checkmark.circle.fill" : nil } } From d85e22c9ff1b23d274515e1b46269c6de52cf1f0 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 18 Apr 2024 07:35:33 +0200 Subject: [PATCH 06/11] clean up match view --- PadelClub.xcodeproj/project.pbxproj | 36 ++++- PadelClub/Data/GroupStage.swift | 3 +- PadelClub/Data/Match.swift | 24 ++-- PadelClub/Data/TeamRegistration.swift | 4 +- PadelClub/Data/Tournament.swift | 23 +++ PadelClub/Extensions/String+Extensions.swift | 5 + PadelClub/ViewModel/MatchDescriptor.swift | 4 +- .../Views/Components/FooterButtonView.swift | 18 +++ .../Views/Components/MatchListView.swift | 21 +-- .../Views/GroupStage/GroupStageView.swift | 25 ++-- .../{ => Components}/MatchDateView.swift | 49 +++++-- .../Components/MatchTeamDetailView.swift | 45 ++++++ .../{ => Components}/PlayerBlockView.swift | 4 +- PadelClub/Views/Match/MatchDetailView.swift | 135 ++++++------------ PadelClub/Views/Score/EditScoreView.swift | 8 +- .../Views/Score/PointSelectionView.swift | 10 +- PadelClub/Views/Score/PointView.swift | 2 +- PadelClub/Views/Score/SetInputView.swift | 2 + .../Team/Components/TeamHeaderView.swift | 43 ++++++ .../Team/Components/TeamWeightView.swift | 38 +++++ PadelClub/Views/Team/TeamRowView.swift | 16 +-- .../Screen/InscriptionManagerView.swift | 23 +-- .../Tournament/TournamentRunningView.swift | 29 ++-- 23 files changed, 367 insertions(+), 200 deletions(-) create mode 100644 PadelClub/Views/Components/FooterButtonView.swift rename PadelClub/Views/Match/{ => Components}/MatchDateView.swift (66%) create mode 100644 PadelClub/Views/Match/Components/MatchTeamDetailView.swift rename PadelClub/Views/Match/{ => Components}/PlayerBlockView.swift (94%) create mode 100644 PadelClub/Views/Team/Components/TeamHeaderView.swift create mode 100644 PadelClub/Views/Team/Components/TeamWeightView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 7debfe4..ac7b61c 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -35,6 +35,10 @@ 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 */; }; + FF025AD82BD0C10F00A86CF8 /* TeamHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AD72BD0C10F00A86CF8 /* TeamHeaderView.swift */; }; + FF025ADB2BD0C2D000A86CF8 /* MatchTeamDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADA2BD0C2D000A86CF8 /* MatchTeamDetailView.swift */; }; + FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADC2BD0C94300A86CF8 /* FooterButtonView.swift */; }; + FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADE2BD0CE0A00A86CF8 /* TeamWeightView.swift */; }; FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */; }; FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */; }; FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */; }; @@ -313,6 +317,10 @@ 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 = ""; }; + FF025AD72BD0C10F00A86CF8 /* TeamHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamHeaderView.swift; sourceTree = ""; }; + FF025ADA2BD0C2D000A86CF8 /* MatchTeamDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTeamDetailView.swift; sourceTree = ""; }; + FF025ADC2BD0C94300A86CF8 /* FooterButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FooterButtonView.swift; sourceTree = ""; }; + FF025ADE2BD0CE0A00A86CF8 /* TeamWeightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamWeightView.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 = ""; }; FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPopoverView.swift; sourceTree = ""; }; @@ -711,12 +719,32 @@ C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */, FF5DA1942BB927E800A33061 /* GenericDestinationPickerView.swift */, FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */, + FF025ADC2BD0C94300A86CF8 /* FooterButtonView.swift */, FFBF065D2BBD8040009D6715 /* MatchListView.swift */, FF967CF72BAEDF0000A9A3BD /* Labels.swift */, ); path = Components; sourceTree = ""; }; + FF025AD62BD0C0FB00A86CF8 /* Components */ = { + isa = PBXGroup; + children = ( + FF025AD72BD0C10F00A86CF8 /* TeamHeaderView.swift */, + FF025ADE2BD0CE0A00A86CF8 /* TeamWeightView.swift */, + ); + path = Components; + sourceTree = ""; + }; + FF025AD92BD0C2BD00A86CF8 /* Components */ = { + isa = PBXGroup; + children = ( + FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */, + FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */, + FF025ADA2BD0C2D000A86CF8 /* MatchTeamDetailView.swift */, + ); + path = Components; + sourceTree = ""; + }; FF089EB02BB001EA00F0AEC7 /* Components */ = { isa = PBXGroup; children = ( @@ -1012,8 +1040,7 @@ FF967D052BAF3C4200A9A3BD /* MatchSetupView.swift */, FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */, FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */, - FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */, - FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */, + FF025AD92BD0C2BD00A86CF8 /* Components */, ); path = Match; sourceTree = ""; @@ -1025,6 +1052,7 @@ FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */, FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */, FF1162862BD004AD000C4809 /* EditingTeamView.swift */, + FF025AD62BD0C0FB00A86CF8 /* Components */, ); path = Team; sourceTree = ""; @@ -1348,6 +1376,7 @@ FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */, FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */, FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */, + FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */, FF9268012BCE94920080F940 /* SeedsCallingView.swift in Sources */, FF9268092BCEDC2C0080F940 /* CallView.swift in Sources */, FF5D0D742BB41DF8005CB568 /* Color+Extensions.swift in Sources */, @@ -1404,6 +1433,7 @@ FF1CBC222BB53E590036DAAB /* FederalTournamentHolder.swift in Sources */, C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */, FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */, + FF025AD82BD0C10F00A86CF8 /* TeamHeaderView.swift in Sources */, FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */, FFF964572BC26B3400EEF017 /* RoundScheduleEditorView.swift in Sources */, FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */, @@ -1412,6 +1442,7 @@ FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */, FF3B60A32BC49BBC008C2E66 /* MatchScheduler.swift in Sources */, FF11627A2BCF8109000C4809 /* CallMessageCustomizationView.swift in Sources */, + FF025ADB2BD0C2D000A86CF8 /* MatchTeamDetailView.swift in Sources */, FF5DA1952BB927E800A33061 /* GenericDestinationPickerView.swift in Sources */, FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */, C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */, @@ -1499,6 +1530,7 @@ FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */, + FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */, FF5D0D852BB48997005CB568 /* RankCalculatorView.swift in Sources */, FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */, FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */, diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 723cc98..1f3e144 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -136,7 +136,8 @@ class GroupStage: ModelObject, Storable { } func availableToStart() -> [Match] { - playedMatches().filter({ $0.canBeStarted() && $0.isRunning() == false }) + let runningMatches = runningMatches() + return playedMatches().filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }) } func runningMatches() -> [Match] { diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index ae76ae3..ccd4c18 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -349,24 +349,26 @@ class Match: ModelObject, Storable { court = String(courtIndex) } - func canBeStarted() -> Bool { + func canBeStarted(inMatches matches: [Match]) -> Bool { let teams = teams() guard teams.count == 2 else { return false } guard hasEnded() == false else { return false } guard hasStarted() == false else { return false } - return teams.allSatisfy({ $0.canPlay() && isTeamPlaying($0) == false }) + return teams.allSatisfy({ $0.canPlay() && isTeamPlaying($0, inMatches: matches) == false }) } - func isTeamPlaying(_ team: TeamRegistration) -> Bool { - if isGroupStage() { - let isPlaying = groupStageObject?.runningMatches().filter({ $0.teams().contains(team) }).isEmpty == false - return isPlaying - } else { - //todo - return false - } + func isTeamPlaying(_ team: TeamRegistration, inMatches matches: [Match]) -> Bool { + matches.filter({ $0.teams().contains(team) }).isEmpty == false } - + + var computedStartDateForSorting: Date { + startDate ?? .distantFuture + } + + var computedEndDateForSorting: Date { + endDate ?? .distantFuture + } + func isReady() -> Bool { teams().count == 2 } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 65bc7ed..706508c 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -128,9 +128,9 @@ class TeamRegistration: ModelObject, Storable { func teamLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { case .wide: - unsortedPlayers().map { $0.playerLabel(displayStyle) }.joined(separator: " & ") + players().map { $0.playerLabel(displayStyle) }.joined(separator: " & ") case .short: - unsortedPlayers().map { $0.playerLabel(.wide) }.joined(separator: "\n") + players().map { $0.playerLabel(.wide) }.joined(separator: "\n") } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index c57a020..95791bc 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -562,6 +562,25 @@ class Tournament : ModelObject, Storable { return false } + func availableToStart(_ allMatches: [Match]) -> [Match] { + let runningMatches = allMatches.filter({ $0.isRunning() && $0.isReady() }) + return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting) + } + + func runningMatches(_ allMatches: [Match]) -> [Match] { + allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) + } + + func readyMatches(_ allMatches: [Match]) -> [Match] { + return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) + } + + func finishedMatches(_ allMatches: [Match], limit: Int? = nil) -> [Match] { + let _limit = limit ?? courtCount + return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(_limit)) + } + + func lockRegistration() { closedRegistrationDate = Date() let count = selectedSortedTeams().count @@ -880,6 +899,10 @@ class Tournament : ModelObject, Storable { entryFee == nil || entryFee == 0 } + func indexOf(team: TeamRegistration) -> Int? { + selectedSortedTeams().firstIndex(where: { $0.id == team.id }) + } + func addTeam(_ players: Set) -> TeamRegistration { let team = TeamRegistration(tournament: id, registrationDate: Date()) team.tournamentCategory = tournamentCategory diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 1b233af..b515c10 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -159,3 +159,8 @@ extension String { } } } + +extension StringProtocol { + var firstUppercased: String { prefix(1).uppercased() + dropFirst() } + var firstCapitalized: String { prefix(1).capitalized + dropFirst() } +} diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift index 1d2d86a..0cd3a14 100644 --- a/PadelClub/ViewModel/MatchDescriptor.swift +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -29,8 +29,8 @@ class MatchDescriptor: ObservableObject { } let teamOne = match?.team(.one) let teamTwo = match?.team(.two) - self.teamLabelOne = teamOne?.teamLabel() ?? "" - self.teamLabelTwo = teamTwo?.teamLabel() ?? "" + self.teamLabelOne = teamOne?.teamLabel(.short) ?? "" + self.teamLabelTwo = teamTwo?.teamLabel(.short) ?? "" if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.score { diff --git a/PadelClub/Views/Components/FooterButtonView.swift b/PadelClub/Views/Components/FooterButtonView.swift new file mode 100644 index 0000000..2d7f99b --- /dev/null +++ b/PadelClub/Views/Components/FooterButtonView.swift @@ -0,0 +1,18 @@ +// +// FooterButtonView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct FooterButtonView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + FooterButtonView() +} diff --git a/PadelClub/Views/Components/MatchListView.swift b/PadelClub/Views/Components/MatchListView.swift index 4f36123..1445dcb 100644 --- a/PadelClub/Views/Components/MatchListView.swift +++ b/PadelClub/Views/Components/MatchListView.swift @@ -19,27 +19,20 @@ struct MatchListView: View { var body: some View { if matches.isEmpty == false { Section { - if isExpanded { + DisclosureGroup(isExpanded: $isExpanded) { ForEach(matches) { match in MatchRowView(match: match, matchViewStyle: matchViewStyle) + .listRowInsets(EdgeInsets()) } - } - } header: { - Button { - isExpanded.toggle() } label: { - HStack { - Text(section.capitalized) - Spacer() - Text(matches.count.formatted()) - Image(systemName: isExpanded ? "chevron.down.circle" : "chevron.right.circle") + LabeledContent { + Text(matches.count.formatted() + " match" + matches.count.pluralSuffix) + .foregroundStyle(.master) + } label: { + Text(section.firstCapitalized) } - .contentShape(Rectangle()) } - .buttonStyle(.plain) - .frame(maxWidth: .infinity) } - .headerProminence(.increased) } } } diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 2789618..f276a95 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -34,26 +34,33 @@ struct GroupStageView: View { Section { _groupStageView() } header: { + if let startDate = groupStage.startDate { + Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute())) + } + } footer: { HStack { - if let startDate = groupStage.startDate { - Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute())) - } - Spacer() - Button { - if sortingMode == .weight { - sortingMode = .score + if sortingMode == .auto { + if groupStage.hasEnded() { + sortingMode = .weight + } else { + sortingMode = .score + } + } else if sortingMode == .weight { + sortingMode = .weight } else { sortingMode = .weight } } label: { Label(sortByScore ? "tri par score" : "tri par poids", systemImage: "arrow.up.arrow.down").labelStyle(.titleOnly) + .underline() } + .buttonStyle(.borderless) } - .buttonStyle(.plain) } - + .headerProminence(.increased) + MatchListView(section: "disponible", matches: groupStage.availableToStart()).id(UUID()) MatchListView(section: "en cours", matches: groupStage.runningMatches()).id(UUID()) MatchListView(section: "à lancer", matches: groupStage.readyMatches()).id(UUID()) diff --git a/PadelClub/Views/Match/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift similarity index 66% rename from PadelClub/Views/Match/MatchDateView.swift rename to PadelClub/Views/Match/Components/MatchDateView.swift index 9dd35c5..4665e0b 100644 --- a/PadelClub/Views/Match/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -8,13 +8,14 @@ import SwiftUI struct MatchDateView: View { + @EnvironmentObject var dataStore: DataStore var match: Match var showPrefix: Bool = false var body: some View { Menu { - if match.startDate == nil { - Button("Commencer") { + if match.startDate == nil && match.isReady() { + Button("Démarrer") { match.startDate = Date() save() } @@ -23,12 +24,20 @@ struct MatchDateView: View { save() } } else { - Button("Recommencer") { - match.startDate = Date() - match.endDate = nil - save() + if match.isReady() { + Button("Démarrer maintenant") { + match.startDate = Date() + match.endDate = nil + save() + } + } else { + Button("Décaler de \(match.matchFormat.estimatedDuration) minutes") { + match.startDate = match.startDate?.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60.0) + match.endDate = nil + save() + } } - Button("Remise à zéro") { + Button("Retirer l'horaire") { match.startDate = nil match.endDate = nil save() @@ -50,8 +59,16 @@ struct MatchDateView: View { if showPrefix { Text("en cours").font(.footnote).foregroundStyle(.secondary) } - Text(startDate, style: .timer) - .monospacedDigit() + if match.isReady() { + Text(startDate, style: .timer) + .monospacedDigit() + .foregroundStyle(Color.master) + .underline() + } else { + Text("en retard") + .foregroundStyle(Color.master) + .underline() + } } else if startDate.timeIntervalSinceNow <= 7200 && showPrefix { if showPrefix { Text("démarre dans") @@ -59,15 +76,21 @@ struct MatchDateView: View { } Text(startDate, style: .timer) .monospacedDigit() + .foregroundStyle(Color.master) + .underline() } else { if showPrefix { Text("le " + startDate.formatted(date: .abbreviated, time: .omitted)) .font(.footnote).foregroundStyle(.secondary) Text("à " + startDate.formatted(date: .omitted, time: .shortened)) .monospacedDigit() + .foregroundStyle(Color.master) + .underline() } else { Text(startDate.formatted(date: .abbreviated, time: .shortened)) .monospacedDigit() + .foregroundStyle(Color.master) + .underline() } } } @@ -81,11 +104,15 @@ struct MatchDateView: View { } Text(duration) .monospacedDigit() + .foregroundStyle(Color.master) + .underline() } if match.startDate == nil && match.hasEnded() == false { Text("démarrage").font(.footnote).foregroundStyle(.secondary) Text("non défini") + .foregroundStyle(Color.master) + .underline() } } } @@ -94,9 +121,7 @@ struct MatchDateView: View { func save() { do { -// match.currentTournament?.objectWillChange.send() -// match.objectWillChange.send() -// try viewContext.save() + try dataStore.matches.addOrUpdate(instance: match) } 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. diff --git a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift new file mode 100644 index 0000000..b09a46e --- /dev/null +++ b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift @@ -0,0 +1,45 @@ +// +// MatchTeamDetailView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct MatchTeamDetailView: View { + let match: Match + + var body: some View { + NavigationStack { + let tournament = match.currentTournament() + List { + if let teamOne = match.team(.one) { + _teamDetailView(teamOne, inTournament: tournament) + } + if let teamTwo = match.team(.two) { + _teamDetailView(teamTwo, inTournament: tournament) + } + } + .headerProminence(.increased) + .tint(.master) + } + .presentationDetents([.fraction(0.66)]) + } + + @ViewBuilder + private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { + Section { + ForEach(team.players()) { player in + EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) + } + } header: { + TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team), tournament: nil) + } + } + +} + +#Preview { + MatchTeamDetailView(match: Match.mock()) +} diff --git a/PadelClub/Views/Match/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift similarity index 94% rename from PadelClub/Views/Match/PlayerBlockView.swift rename to PadelClub/Views/Match/Components/PlayerBlockView.swift index 68ee799..29b9127 100644 --- a/PadelClub/Views/Match/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -79,7 +79,7 @@ struct PlayerBlockView: View { Text("WO") } - if hideScore == false { + if hideScore == false && scores.isEmpty == false { ForEach(scores.indices, id: \.self) { index in let string = scores[index] if string.isEmpty == false { @@ -96,6 +96,8 @@ struct PlayerBlockView: View { .lineLimit(1) } } + } else if let team { + TeamWeightView(team: team, teamPosition: teamPosition) } } } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 8504db5..d12d91e 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -48,86 +48,40 @@ struct MatchDetailView: View { _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.removeCourt() + Menu { + Button("Non défini") { + match.removeCourt() + save() + } + ForEach(1...match.courtCount(), id: \.self) { courtIndex in + Button("Terrain #\(courtIndex.formatted())") { + match.setCourt(courtIndex) save() } - ForEach(1...match.courtCount(), id: \.self) { courtIndex in - Button("Terrain #\(courtIndex.formatted())") { - match.setCourt(courtIndex) - save() - } - } - } label: { - VStack(alignment: .leading) { - Text("terrain").font(.footnote).foregroundStyle(.secondary) - if let court = match.court { - Text("#" + court) - } else { - Text("Choisir") - } + } + } label: { + VStack(alignment: .leading) { + Text("terrain").font(.footnote).foregroundStyle(.secondary) + if let court = match.court { + Text("#" + court) + .foregroundStyle(Color.master) + .underline() + } else { + Text("Choisir") + .foregroundStyle(Color.master) + .underline() } } - .buttonStyle(.plain) } Spacer() MatchDateView(match: match, showPrefix: true) } .font(.title) + .buttonStyle(.plain) } footer: { // if match.hasWalkoutTeam() == false { // if let weatherData = match.weatherData { @@ -151,7 +105,6 @@ struct MatchDetailView: View { Section { MatchSummaryView(match: match, matchViewStyle: .plainStyle) - } header: { } footer: { if match.isEmpty() == false { HStack { @@ -171,34 +124,34 @@ struct MatchDetailView: View { } } - Section { - ForEach(match.teams()) { team in - ForEach(team.players().filter({ $0.hasPaid() == false })) { player in - HStack { - Text(player.playerLabel()) - Spacer() - //PlayerPayView(player: player) + let players = match.teams().flatMap { $0.players() } + let unpaid = players.filter({ $0.hasPaid() == false }) + + if unpaid.isEmpty == false { + Section { + DisclosureGroup { + ForEach(unpaid) { player in + LabeledContent { + PlayerPayView(player: player) + } label: { + Text(player.playerLabel()) + } + } + } label: { + LabeledContent { + Text(unpaid.count.formatted() + " / " + players.count.formatted()) + } label: { + Text("Encaissement manquant") } } } } - 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(isPresented: $showDetails) { + MatchTeamDetailView(match: match) + } .sheet(item: $scoreType, onDismiss: { if match.hasEnded() { dismiss() @@ -206,6 +159,7 @@ struct MatchDetailView: View { }) { scoreType in let matchDescriptor = MatchDescriptor(match: match) EditScoreView(matchDescriptor: matchDescriptor) + .tint(.master) // switch scoreType { // case .edition: @@ -305,7 +259,8 @@ struct MatchDetailView: View { // } // } .navigationTitle(match.matchTitle()) - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } enum ScoreType: Int, Identifiable, Hashable { diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 3c0cd33..a1bba95 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -22,7 +22,10 @@ struct EditScoreView: View { Form { Section { Text(matchDescriptor.teamLabelOne) - Text(matchDescriptor.teamLabelTwo) + HStack { + Spacer() + Text(matchDescriptor.teamLabelTwo).multilineTextAlignment(.trailing) + } } footer: { HStack { Menu { @@ -37,7 +40,8 @@ struct EditScoreView: View { Text(matchDescriptor.teamLabelTwo) } } label: { - Text("Forfait") + Text("Forfait d'une équipe ?") + .underline() } Spacer() diff --git a/PadelClub/Views/Score/PointSelectionView.swift b/PadelClub/Views/Score/PointSelectionView.swift index 40d0579..1b70405 100644 --- a/PadelClub/Views/Score/PointSelectionView.swift +++ b/PadelClub/Views/Score/PointSelectionView.swift @@ -13,8 +13,8 @@ struct PointSelectionView: View { var possibleValues: [Int] var disableValues: [Int] = [] var deleteAction: () -> () - let gridItems: [GridItem] = [GridItem(.adaptive(minimum: 65), spacing: 20)] - + let columns = Array(repeating: GridItem(.flexible()), count: 3) + init(valueSelected: Binding, values: [Int], possibleValues: [Int], disableValues: [Int], deleteAction: @escaping () -> Void) { _valueSelected = valueSelected @@ -26,12 +26,13 @@ struct PointSelectionView: View { var body: some View { - LazyVGrid(columns: gridItems, alignment: .center, spacing: 20) { + LazyVGrid(columns: columns, alignment: .center, spacing: 8) { ForEach(possibleValues, id: \.self) { value in Button { valueSelected = value } label: { PointView(value: "\(value).circle.fill") + .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) @@ -41,10 +42,11 @@ struct PointSelectionView: View { deleteAction() } label: { PointView(value: "delete.left.fill") + .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) } - .padding() + .padding(8) } } diff --git a/PadelClub/Views/Score/PointView.swift b/PadelClub/Views/Score/PointView.swift index 471c749..c72fb1d 100644 --- a/PadelClub/Views/Score/PointView.swift +++ b/PadelClub/Views/Score/PointView.swift @@ -15,7 +15,7 @@ struct PointView: View { .resizable() .aspectRatio(contentMode: .fit) .font(.largeTitle) - .frame(height: 40) + .frame(height: 36) } } diff --git a/PadelClub/Views/Score/SetInputView.swift b/PadelClub/Views/Score/SetInputView.swift index ebcea2f..d69919b 100644 --- a/PadelClub/Views/Score/SetInputView.swift +++ b/PadelClub/Views/Score/SetInputView.swift @@ -124,12 +124,14 @@ struct SetInputView: View { Section { DisclosureGroup(isExpanded: $showSetInputView) { PointSelectionView(valueSelected: currentValue, values: possibleValues(), possibleValues: setFormat.possibleValues, disableValues: disableValues, deleteAction: deleteLastValue) + .listRowInsets(EdgeInsets(top: -8, leading: -20, bottom: 0, trailing: 0)) } label: { SetLabelView(initialValueLeft: $setDescriptor.valueTeamOne, initialValueRight: $setDescriptor.valueTeamTwo, shouldDisplaySteppers: isMainViewTieBreakView) } if showTieBreakView { DisclosureGroup(isExpanded: $showTieBreakInputView) { PointSelectionView(valueSelected: currentTiebreakValue, values: tieBreakPossibleValues(), possibleValues: SetFormat.six.possibleValues, disableValues: disableTieBreakValues, deleteAction: deleteLastTiebreakValue) + .listRowInsets(EdgeInsets(top: -8, leading: -20, bottom: 0, trailing: 0)) } label: { SetLabelView(initialValueLeft: $setDescriptor.tieBreakValueTeamOne, initialValueRight: $setDescriptor.tieBreakValueTeamTwo, shouldDisplaySteppers: showTieBreakInputView, isTieBreak: true) } diff --git a/PadelClub/Views/Team/Components/TeamHeaderView.swift b/PadelClub/Views/Team/Components/TeamHeaderView.swift new file mode 100644 index 0000000..76818a6 --- /dev/null +++ b/PadelClub/Views/Team/Components/TeamHeaderView.swift @@ -0,0 +1,43 @@ +// +// TeamHeaderView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct TeamHeaderView: View { + var team: TeamRegistration + var teamIndex: Int? + var tournament: Tournament? + + var body: some View { + _teamHeaderView(team, teamIndex: teamIndex) + } + + private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View { + HStack { + if let teamIndex { + Text("#" + (teamIndex + 1).formatted()) + } + + if team.unsortedPlayers().isEmpty == false { + Text(team.weight.formatted()) + } + if team.isWildCard() { + Text("wildcard").italic().font(.caption) + } + Spacer() + if team.walkOut { + Text("WO") + } else if let teamIndex, let tournament { + Text(tournament.cutLabel(index: teamIndex)) + } + } + } +} + +#Preview { + TeamHeaderView(team: TeamRegistration.mock(), teamIndex: 1, tournament: nil) +} diff --git a/PadelClub/Views/Team/Components/TeamWeightView.swift b/PadelClub/Views/Team/Components/TeamWeightView.swift new file mode 100644 index 0000000..fc48af5 --- /dev/null +++ b/PadelClub/Views/Team/Components/TeamWeightView.swift @@ -0,0 +1,38 @@ +// +// TeamWeightView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct TeamWeightView: View { + var team: TeamRegistration + var teamPosition: TeamPosition? = nil + + var body: some View { + VStack(alignment: .trailing, spacing: 0) { + if teamPosition == .one || teamPosition == nil { + Text(team.weight.formatted()) + .monospacedDigit() + .font(.caption) + } + if let teams = team.tournamentObject()?.selectedSortedTeams(), let index = team.index(in: teams) { + Text("#" + (index + 1).formatted(.number.precision(.integerLength(2...3)))) + .monospacedDigit() + .font(.title) + } + if teamPosition == .two { + Text(team.weight.formatted()) + .monospacedDigit() + .font(.caption) + + } + } + } +} + +#Preview { + TeamWeightView(team: TeamRegistration.mock(), teamPosition: .one) +} diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 4eebb7e..a6972c5 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -14,21 +14,7 @@ struct TeamRowView: View { var body: some View { LabeledContent { - VStack(alignment: .trailing, spacing: 0) { - if teamPosition == .one || teamPosition == nil { - Text(team.weight.formatted()) - .font(.caption) - } - if let teams = team.tournamentObject()?.selectedSortedTeams(), let index = team.index(in: teams) { - Text("#" + (index + 1).formatted()) - .font(.title) - } - if teamPosition == .two { - Text(team.weight.formatted()) - .font(.caption) - - } - } + TeamWeightView(team: team, teamPosition: teamPosition) } label: { Text(team.teamLabel(.short)) if let callDate = team.callDate, displayCallDate { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index dd6a40b..dee982e 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -239,7 +239,7 @@ struct InscriptionManagerView: View { Section { TeamDetailView(team: team) } header: { - _teamHeaderView(team, teamIndex: teamIndex) + TeamHeaderView(team: team, teamIndex: teamIndex, tournament: tournament) } footer: { _teamFooterView(team) } @@ -717,27 +717,6 @@ struct InscriptionManagerView: View { } } - private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View { - HStack { - if let teamIndex { - Text("#" + (teamIndex + 1).formatted()) - } - - if team.unsortedPlayers().isEmpty == false { - Text(team.weight.formatted()) - } - if team.isWildCard() { - Text("wildcard").italic().font(.caption) - } - Spacer() - if team.walkOut { - Text("WO") - } else if let teamIndex { - Text(tournament.cutLabel(index: teamIndex)) - } - } - } - private func _teamFooterView(_ team: TeamRegistration) -> some View { HStack { if let formattedRegistrationDate = team.formattedInscriptionDate() { diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index 2bd172b..da4909c 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -44,8 +44,8 @@ struct TournamentRunningView: View { } } - if tournament.groupStages().isEmpty == false { - Section { + Section { + if tournament.groupStages().isEmpty == false { NavigationLink(value: Screen.groupStage) { LabeledContent { Text(tournament.groupStageStatus()) @@ -55,19 +55,24 @@ struct TournamentRunningView: View { } } } - } - - Section { - NavigationLink(value: Screen.round) { - LabeledContent { - Text(tournament.bracketStatus()) - .foregroundStyle(.master) - } label: { - Text("Tableau") + + if tournament.rounds().isEmpty == false { + NavigationLink(value: Screen.round) { + LabeledContent { + Text(tournament.bracketStatus()) + .foregroundStyle(.master) + } label: { + Text("Tableau") + } } } } - + + let allMatches = tournament.allMatches() + MatchListView(section: "en cours", matches: tournament.runningMatches(allMatches)).id(UUID()) + MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches)).id(UUID()) + MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches)).id(UUID()) + MatchListView(section: "terminés", matches: tournament.finishedMatches(allMatches)).id(UUID()) } } From f13a71675dd21daf34d532e64ec313dabeca1340 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 18 Apr 2024 09:44:08 +0200 Subject: [PATCH 07/11] enhance tournament settings screen --- PadelClub.xcodeproj/project.pbxproj | 12 ++ PadelClub/Data/GroupStage.swift | 4 +- PadelClub/Data/Round.swift | 4 +- PadelClub/ViewModel/AgendaDestination.swift | 2 +- PadelClub/ViewModel/Selectable.swift | 31 +++- .../GenericDestinationPickerView.swift | 6 +- .../Views/GroupStage/GroupStagesView.swift | 2 +- .../TournamentClubSettingsView.swift | 68 ++++++++ .../TournamentGeneralSettingsView.swift | 78 +++++++++ .../TournamentMatchFormatsSettingsView.swift | 20 +++ .../Screen/TournamentCallView.swift | 6 +- .../Screen/TournamentCashierView.swift | 8 +- .../Screen/TournamentScheduleView.swift | 2 +- .../Screen/TournamentSettingsView.swift | 153 +++++++----------- 14 files changed, 280 insertions(+), 116 deletions(-) create mode 100644 PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift create mode 100644 PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift create mode 100644 PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index ac7b61c..cc5fbe0 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -39,6 +39,9 @@ FF025ADB2BD0C2D000A86CF8 /* MatchTeamDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADA2BD0C2D000A86CF8 /* MatchTeamDetailView.swift */; }; FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADC2BD0C94300A86CF8 /* FooterButtonView.swift */; }; FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADE2BD0CE0A00A86CF8 /* TeamWeightView.swift */; }; + FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */; }; + FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; }; + FF025AE52BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */; }; FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */; }; FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */; }; FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */; }; @@ -321,6 +324,9 @@ FF025ADA2BD0C2D000A86CF8 /* MatchTeamDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTeamDetailView.swift; sourceTree = ""; }; FF025ADC2BD0C94300A86CF8 /* FooterButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FooterButtonView.swift; sourceTree = ""; }; FF025ADE2BD0CE0A00A86CF8 /* TeamWeightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamWeightView.swift; sourceTree = ""; }; + FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentClubSettingsView.swift; sourceTree = ""; }; + FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentMatchFormatsSettingsView.swift; sourceTree = ""; }; + FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentGeneralSettingsView.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 = ""; }; FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPopoverView.swift; sourceTree = ""; }; @@ -1006,6 +1012,9 @@ FF8F26492BAE0B4100650388 /* TournamentLevelPickerView.swift */, FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */, FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */, + FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */, + FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */, + FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */, ); path = Components; sourceTree = ""; @@ -1374,6 +1383,7 @@ FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */, FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */, FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */, + FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */, FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */, FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */, FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */, @@ -1460,6 +1470,7 @@ FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */, FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */, FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */, + FF025AE52BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift in Sources */, FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */, FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, @@ -1475,6 +1486,7 @@ FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, + FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */, FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */, FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */, FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */, diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 1f3e144..7378361 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -281,7 +281,7 @@ extension GroupStage: Selectable { runningMatches().count } - func badgeImage() -> String? { - hasEnded() ? "checkmark.circle.fill" : nil + func badgeImage() -> Badge? { + hasEnded() ? .checkmark : nil } } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 0380d30..823e143 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -419,7 +419,7 @@ extension Round: Selectable { } } - func badgeImage() -> String? { - hasEnded() ? "checkmark.circle.fill" : nil + func badgeImage() -> Badge? { + hasEnded() ? .checkmark : nil } } diff --git a/PadelClub/ViewModel/AgendaDestination.swift b/PadelClub/ViewModel/AgendaDestination.swift index 2f05f66..4070cc8 100644 --- a/PadelClub/ViewModel/AgendaDestination.swift +++ b/PadelClub/ViewModel/AgendaDestination.swift @@ -56,7 +56,7 @@ enum AgendaDestination: CaseIterable, Identifiable, Selectable { } } - func badgeImage() -> String? { + func badgeImage() -> Badge? { nil } } diff --git a/PadelClub/ViewModel/Selectable.swift b/PadelClub/ViewModel/Selectable.swift index 0f656cf..7158823 100644 --- a/PadelClub/ViewModel/Selectable.swift +++ b/PadelClub/ViewModel/Selectable.swift @@ -6,9 +6,38 @@ // import Foundation +import SwiftUI protocol Selectable { func selectionLabel() -> String func badgeValue() -> Int? - func badgeImage() -> String? + func badgeImage() -> Badge? +} + +enum Badge { + case checkmark + case xmark + case custom(systemName: String, color: Color) + + func systemName() -> String { + switch self { + case .checkmark: + return "checkmark.circle.fill" + case .xmark: + return "xmark.circle.fill" + case .custom(let systemName, _): + return systemName + } + } + + func color() -> Color { + switch self { + case .checkmark: + .green + case .xmark: + .red + case .custom(_, let color): + color + } + } } diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 13c0adf..4531718 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -45,9 +45,9 @@ struct GenericDestinationPickerView: View { } .buttonStyle(.plain) .overlay(alignment: .bottomTrailing) { - if let image = destination.badgeImage() { - Image(systemName: image) - .foregroundColor(.green) + if let badge = destination.badgeImage() { + Image(systemName: badge.systemName()) + .foregroundColor(badge.color()) .imageScale(.medium) .background ( Color(.systemBackground) diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index 7dd0d07..7cd5f0e 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -42,7 +42,7 @@ struct GroupStagesView: View { } } - func badgeImage() -> String? { + func badgeImage() -> Badge? { nil } } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift new file mode 100644 index 0000000..3ac8a52 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift @@ -0,0 +1,68 @@ +// +// TournamentClubSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct TournamentClubSettingsView: View { + @Environment(Tournament.self) private var tournament: Tournament + @EnvironmentObject var dataStore: DataStore + + var body: some View { + @Bindable var tournament = tournament + List { + let event = tournament.eventObject + let selectedClub = event?.clubObject + Section { + if let selectedClub { + NavigationLink { + ClubDetailView(club: selectedClub, displayContext: .edition) + } label: { + ClubRowView(club: selectedClub) + } + } else { + NavigationLink { + ClubsView() { club in + if let event { + event.club = club.id + try? dataStore.events.addOrUpdate(instance: event) + } else { + let event = Event(club: club.id) + tournament.event = event.id + try? dataStore.events.addOrUpdate(instance: event) + } + } + } label: { + Text("Choisir un club") + } + } + } header: { + Text("Lieu du tournoi") + } footer: { + if let event, selectedClub != nil { + HStack { + Spacer() + Button("modifier", role: .destructive) { + event.club = nil + try? dataStore.events.addOrUpdate(instance: event) + } + } + } + } + + Section { + TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount, max: 100) + } + } + .onDisappear { + try? dataStore.tournaments.addOrUpdate(instance: tournament) + } + } +} + +#Preview { + TournamentClubSettingsView() +} diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift new file mode 100644 index 0000000..094596a --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -0,0 +1,78 @@ +// +// TournamentGeneralSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct TournamentGeneralSettingsView: View { + @Environment(Tournament.self) private var tournament: Tournament + @EnvironmentObject var dataStore: DataStore + + @State private var tournamentName: String = "" + @FocusState private var textFieldIsFocus: Bool + + var body: some View { + @Bindable var tournament = tournament + Form { + Section { + TournamentDatePickerView() + TournamentDurationManagerView() + } + + Section { + TournamentLevelPickerView() + } + + Section { + LabeledContent { + TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $tournament.entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + } label: { + Text("Inscription") + } + } + + Section { + LabeledContent { + TextField("Nom", text: $tournamentName) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + .keyboardType(.alphabet) + .autocorrectionDisabled() + .onSubmit { + if tournamentName.trimmed.isEmpty { + tournament.name = nil + } else { + tournament.name = tournamentName + } + } + } label: { + Text("Nom du tournoi") + } + } + } + .focused($textFieldIsFocus) + .scrollDismissesKeyboard(.immediately) + .navigationTitle("Réglages") + .toolbarBackground(.visible, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .keyboard) { + Button("Valider") { + textFieldIsFocus = false + } + } + } + .onDisappear { + try? dataStore.tournaments.addOrUpdate(instance: tournament) + } + } +} + +#Preview { + TournamentGeneralSettingsView() +} diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift new file mode 100644 index 0000000..c477c54 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift @@ -0,0 +1,20 @@ +// +// TournamentMatchFormatsSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct TournamentMatchFormatsSettingsView: View { + var body: some View { + List { + TournamentFormatSelectionView() + } + } +} + +#Preview { + TournamentMatchFormatsSettingsView() +} diff --git a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift index af1e0d3..8a8dfa3 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift @@ -40,14 +40,14 @@ enum CallDestination: Identifiable, Selectable { } } - func badgeImage() -> String? { + func badgeImage() -> Badge? { switch self { case .seeds(let tournament): let allSeedCalled = tournament.seeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) - return allSeedCalled ? "checkmark.circle.fill" : nil + return allSeedCalled ? .checkmark : nil case .groupStages(let tournament): let allSeedCalled = tournament.groupStageTeams().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) - return allSeedCalled ? "checkmark.circle.fill" : nil + return allSeedCalled ? .checkmark : nil } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift index 1de11e8..4b1e91f 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift @@ -50,16 +50,16 @@ enum CashierDestination: Identifiable, Selectable { } } - func badgeImage() -> String? { + func badgeImage() -> Badge? { switch self { case .summary: return nil case .groupStage(let groupStage): - return groupStage.unsortedPlayers().allSatisfy({ $0.hasPaid() }) ? "checkmark.circle.fill" : nil + return groupStage.unsortedPlayers().allSatisfy({ $0.hasPaid() }) ? .checkmark : nil case .bracket(let round): - return round.seeds().flatMap { $0.unsortedPlayers() }.allSatisfy({ $0.hasPaid() }) ? "checkmark.circle.fill" : nil + return round.seeds().flatMap { $0.unsortedPlayers() }.allSatisfy({ $0.hasPaid() }) ? .checkmark : nil case .all(let tournament): - return tournament.selectedPlayers().allSatisfy({ $0.hasPaid() }) ? "checkmark.circle.fill" : nil + return tournament.selectedPlayers().allSatisfy({ $0.hasPaid() }) ? .checkmark : nil } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift index fccb5a2..82bc8cb 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift @@ -42,7 +42,7 @@ enum ScheduleDestination: String, Identifiable, Selectable { nil } - func badgeImage() -> String? { + func badgeImage() -> Badge? { nil } diff --git a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift index b05d84b..a984d14 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift @@ -7,112 +7,69 @@ import SwiftUI -struct TournamentSettingsView: View { - @Environment(Tournament.self) private var tournament: Tournament - @EnvironmentObject var dataStore: DataStore - - @State private var tournamentName: String = "" - @FocusState private var textFieldIsFocus: Bool - - var body: some View { - @Bindable var tournament = tournament - Form { - LabeledContent { - TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $tournament.entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) - .keyboardType(.decimalPad) - .multilineTextAlignment(.trailing) - .frame(maxWidth: .infinity) - } label: { - Text("Inscription") - } - - LabeledContent { - TextField("Nom", text: $tournamentName) - .multilineTextAlignment(.trailing) - .frame(maxWidth: .infinity) - .keyboardType(.alphabet) - .autocorrectionDisabled() - .onSubmit { - if tournamentName.trimmed.isEmpty { - tournament.name = nil - } else { - tournament.name = tournamentName - } - } - } label: { - Text("Nom du tournoi") - } - - TournamentLevelPickerView() - TournamentDurationManagerView() - TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount, max: 100) - TournamentDatePickerView() - +enum TournamentSettings: Identifiable, Selectable { + case general + case club(Tournament) + case matchFormats - let event = tournament.eventObject - let selectedClub = event?.clubObject - Section { - if let selectedClub { - NavigationLink { - ClubDetailView(club: selectedClub, displayContext: .edition) - } label: { - ClubRowView(club: selectedClub) - } - } else { - NavigationLink { - ClubsView() { club in - if let event { - event.club = club.id - try? dataStore.events.addOrUpdate(instance: event) - } else { - let event = Event(club: club.id) - tournament.event = event.id - try? dataStore.events.addOrUpdate(instance: event) - } - } - } label: { - Text("Choisir un club") - } - } - } header: { - Text("Lieu du tournoi") - } footer: { - if let event, selectedClub != nil { - HStack { - Spacer() - Button("modifier", role: .destructive) { - event.club = nil - try? dataStore.events.addOrUpdate(instance: event) - } - } - } - } - - TournamentFormatSelectionView() + var id: String { String(describing: self) } + func selectionLabel() -> String { + switch self { + case .matchFormats: + return "Formats de jeu" + case .general: + return "Général" + case .club: + return "Club" } - .focused($textFieldIsFocus) - .scrollDismissesKeyboard(.immediately) - .navigationTitle("Réglages") - .toolbarBackground(.visible, for: .navigationBar) - .toolbar { - ToolbarItem(placement: .keyboard) { - Button("Valider") { - textFieldIsFocus = false - } + } + + func badgeValue() -> Int? { + nil + } + + func badgeImage() -> Badge? { + switch self { + case .club(let tournament): + if tournament.club() != nil { + return .checkmark + } else { + return .xmark } + default: + return nil } - .onDisappear { - try? dataStore.tournaments.addOrUpdate(instance: tournament) + } +} + +struct TournamentSettingsView: View { + @State private var selectedDestination: TournamentSettings? = .general + @Environment(Tournament.self) var tournament: Tournament + + private func destinations() -> [TournamentSettings] { + [.general, .club(tournament), .matchFormats] + } + + var body: some View { + VStack(spacing: 0) { + GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: destinations(), nilDestinationIsValid: false) + switch selectedDestination! { + case .matchFormats: + TournamentMatchFormatsSettingsView() + case .general: + TournamentGeneralSettingsView() + case .club: + TournamentClubSettingsView() + } } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Réglages") } + } #Preview { - Group { - - TournamentSettingsView() - .environmentObject(DataStore.shared) - .environment(Tournament.mock()) - } + TournamentSettingsView() } From b9f204854600ecb94417b062eed5372e52d2d12e Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 18 Apr 2024 22:52:41 +0200 Subject: [PATCH 08/11] clean up fix micro storage --- PadelClub.xcodeproj/project.pbxproj | 28 ++++-- PadelClub/Data/AppSettings.swift | 36 +++++++- PadelClub/Data/Club.swift | 11 ++- PadelClub/Data/DataStore.swift | 43 ++++++--- PadelClub/Data/Event.swift | 1 + .../Data/Federal/FederalTournament.swift | 1 + PadelClub/Data/GroupStage.swift | 11 +++ PadelClub/Data/Match.swift | 8 +- PadelClub/Data/MockData.swift | 9 +- PadelClub/Data/MonthData.swift | 53 +++++++++++ PadelClub/Data/Round.swift | 9 ++ PadelClub/Data/Tournament.swift | 50 +++++++---- PadelClub/Data/User.swift | 13 +-- PadelClub/Manager/ContactManager.swift | 14 +-- PadelClub/Manager/PadelRule.swift | 16 ++-- PadelClub/Manager/SourceFileManager.swift | 2 +- PadelClub/ViewModel/AppScreen.swift | 13 +++ PadelClub/ViewModel/MatchScheduler.swift | 8 +- PadelClub/ViewModel/NavigationViewModel.swift | 1 + .../CallMessageCustomizationView.swift | 38 ++++---- .../Views/Calling/CallSettingsView.swift | 14 ++- PadelClub/Views/Club/ClubSearchView.swift | 1 + PadelClub/Views/ClubView.swift | 23 ----- PadelClub/Views/Components/Labels.swift | 4 +- PadelClub/Views/Components/StepperView.swift | 8 +- PadelClub/Views/ContentView.swift | 82 ----------------- .../Match/Components/MatchDateView.swift | 6 +- PadelClub/Views/Match/MatchDetailView.swift | 6 +- .../Navigation/Agenda/ActivityView.swift | 6 +- PadelClub/Views/Navigation/MainView.swift | 33 ++++--- .../Organizer/TournamentOrganizerView.swift | 1 + .../Views/Navigation/PadelClubView.swift | 48 ++++++++-- .../Toolbox/DurationSettingsView.swift | 25 ++++++ .../Toolbox/GlobalSettingsView.swift | 67 ++++++++++++++ .../Toolbox/MatchFormatStorageView.swift | 50 +++++++++++ .../Navigation/Toolbox/ToolboxView.swift | 33 +++++-- .../Views/Navigation/Umpire/UmpireView.swift | 2 - .../Views/Planning/PlanningSettingsView.swift | 5 +- .../Views/Shared/MatchFormatPickerView.swift | 3 +- .../Shared/SelectablePlayerListView.swift | 9 +- .../TournamentClubSettingsView.swift | 2 +- .../TournamentGeneralSettingsView.swift | 24 ++++- .../TournamentMatchFormatsSettingsView.swift | 88 ++++++++++++++++++- .../Views/Tournament/TournamentInitView.swift | 2 + .../Views/Tournament/TournamentView.swift | 6 +- 45 files changed, 637 insertions(+), 276 deletions(-) create mode 100644 PadelClub/Data/MonthData.swift create mode 100644 PadelClub/ViewModel/AppScreen.swift delete mode 100644 PadelClub/Views/ClubView.swift delete mode 100644 PadelClub/Views/ContentView.swift create mode 100644 PadelClub/Views/Navigation/Toolbox/DurationSettingsView.swift create mode 100644 PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift create mode 100644 PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index cc5fbe0..a5b55a7 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4002B6D249D002A7B48 /* PadelClubApp.swift */; }; - C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4022B6D249D002A7B48 /* ContentView.swift */; }; C425D4052B6D249E002A7B48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4042B6D249E002A7B48 /* Assets.xcassets */; }; C425D4082B6D249E002A7B48 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4072B6D249E002A7B48 /* Preview Assets.xcassets */; }; C425D4122B6D249E002A7B48 /* PadelClubTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4112B6D249E002A7B48 /* PadelClubTests.swift */; }; @@ -20,7 +19,6 @@ C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D592B6D383C00ADC637 /* Tournament.swift */; }; C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */; }; C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D622B6D3D6500ADC637 /* Club.swift */; }; - C4A47D742B72881F00ADC637 /* ClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D732B72881F00ADC637 /* ClubView.swift */; }; C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D762B73789100ADC637 /* TournamentV1.swift */; }; C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D7A2B73C0F900ADC637 /* TournamentV2.swift */; }; C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D7C2B73CDC300ADC637 /* ClubV1.swift */; }; @@ -42,6 +40,11 @@ FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */; }; FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; }; FF025AE52BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */; }; + FF025AE72BD1111000A86CF8 /* GlobalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */; }; + FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; }; + FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AEC2BD1513700A86CF8 /* AppScreen.swift */; }; + FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */; }; + FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */; }; FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */; }; FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */; }; FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */; }; @@ -290,7 +293,6 @@ /* Begin PBXFileReference section */ C425D3FD2B6D249D002A7B48 /* PadelClub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PadelClub.app; sourceTree = BUILT_PRODUCTS_DIR; }; C425D4002B6D249D002A7B48 /* PadelClubApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubApp.swift; sourceTree = ""; }; - C425D4022B6D249D002A7B48 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; C425D4042B6D249E002A7B48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C425D4072B6D249E002A7B48 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; C425D40D2B6D249E002A7B48 /* PadelClubTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PadelClubTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -305,7 +307,6 @@ C4A47D592B6D383C00ADC637 /* Tournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tournament.swift; sourceTree = ""; }; C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = ""; }; C4A47D622B6D3D6500ADC637 /* Club.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Club.swift; sourceTree = ""; }; - C4A47D732B72881F00ADC637 /* ClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubView.swift; sourceTree = ""; }; C4A47D762B73789100ADC637 /* TournamentV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentV1.swift; sourceTree = ""; }; C4A47D7A2B73C0F900ADC637 /* TournamentV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentV2.swift; sourceTree = ""; }; C4A47D7C2B73CDC300ADC637 /* ClubV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubV1.swift; sourceTree = ""; }; @@ -327,6 +328,11 @@ FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentClubSettingsView.swift; sourceTree = ""; }; FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentMatchFormatsSettingsView.swift; sourceTree = ""; }; FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentGeneralSettingsView.swift; sourceTree = ""; }; + FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSettingsView.swift; sourceTree = ""; }; + FF025AE82BD1307E00A86CF8 /* MonthData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthData.swift; sourceTree = ""; }; + FF025AEC2BD1513700A86CF8 /* AppScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppScreen.swift; sourceTree = ""; }; + FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DurationSettingsView.swift; sourceTree = ""; }; + FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatStorageView.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 = ""; }; FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPopoverView.swift; sourceTree = ""; }; @@ -650,6 +656,7 @@ FF967CEF2BAECC0A00A9A3BD /* TeamScore.swift */, C4A47D622B6D3D6500ADC637 /* Club.swift */, FF8F263E2BAD7D5C00650388 /* Event.swift */, + FF025AE82BD1307E00A86CF8 /* MonthData.swift */, FF1DC5522BAB354A00FD8220 /* MockData.swift */, FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */, FF6EC9012B94799200EA7F5A /* Coredata */, @@ -661,8 +668,6 @@ C4A47D722B72881500ADC637 /* Views */ = { isa = PBXGroup; children = ( - C425D4022B6D249D002A7B48 /* ContentView.swift */, - C4A47D732B72881F00ADC637 /* ClubView.swift */, FF39719B2B8DE04B004C4E75 /* Navigation */, FF8F26392BAD526A00650388 /* Event */, FF1DC54D2BAB34FA00FD8220 /* Club */, @@ -920,6 +925,9 @@ children = ( FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */, FF5D0D822BB48997005CB568 /* RankCalculatorView.swift */, + FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */, + FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */, + FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */, ); path = Toolbox; sourceTree = ""; @@ -936,6 +944,7 @@ isa = PBXGroup; children = ( FF7091652B90F0B000AB08DA /* TabDestination.swift */, + FF025AEC2BD1513700A86CF8 /* AppScreen.swift */, FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */, FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */, FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */, @@ -1414,6 +1423,8 @@ FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */, FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, + FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */, + FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */, FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */, FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */, FF92680B2BCEE3E10080F940 /* ContactManager.swift in Sources */, @@ -1426,7 +1437,6 @@ 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 */, @@ -1489,11 +1499,11 @@ FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */, FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */, FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */, + FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */, FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */, FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */, - C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */, FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, @@ -1506,6 +1516,7 @@ FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */, FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */, FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */, + FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */, FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */, C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */, C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */, @@ -1548,6 +1559,7 @@ FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */, FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */, FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */, + FF025AE72BD1111000A86CF8 /* GlobalSettingsView.swift in Sources */, C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/PadelClub/Data/AppSettings.swift b/PadelClub/Data/AppSettings.swift index 864ea23..48909e9 100644 --- a/PadelClub/Data/AppSettings.swift +++ b/PadelClub/Data/AppSettings.swift @@ -7,14 +7,48 @@ import Foundation import LeStorage +import SwiftUI +@Observable class AppSettings: MicroStorable { static var fileName: String { "appsettings.json" } + + var lastDataSource: String? = nil + var callMessageBody : String? = nil + var callMessageSignature: String? = nil + var callDisplayFormat: Bool = false + var callDisplayEntryFee: Bool = false + var callUseFullCustomMessage: Bool = false + var matchFormatsDefaultDuration: [MatchFormat: Int]? = nil + var bracketMatchFormatPreference: Int? + var groupStageMatchFormatPreference: Int? + var loserBracketMatchFormatPreference: Int? required init() { } -// var id: String = Store.randomId() + func saveMatchFormatsDefaultDuration(_ matchFormat: MatchFormat, estimatedDuration: Int) { + if estimatedDuration == matchFormat.defaultEstimatedDuration { + matchFormatsDefaultDuration?.removeValue(forKey: matchFormat) + } else { + matchFormatsDefaultDuration = matchFormatsDefaultDuration ?? [MatchFormat: Int]() + matchFormatsDefaultDuration?[matchFormat] = estimatedDuration + } + } + + enum CodingKeys: String, CodingKey { + case _lastDataSource = "lastDataSource" + case _callMessageBody = "callMessageBody" + case _callMessageSignature = "callMessageSignature" + case _callDisplayFormat = "callDisplayFormat" + case _callDisplayEntryFee = "callDisplayEntryFee" + case _callUseFullCustomMessage = "callUseFullCustomMessage" + case _matchFormatsDefaultDuration = "matchFormatsDefaultDuration" + case _bracketMatchFormatPreference = "bracketMatchFormatPreference" + case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference" + case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference" + + } } diff --git a/PadelClub/Data/Club.swift b/PadelClub/Data/Club.swift index 7596ec8..aca7233 100644 --- a/PadelClub/Data/Club.swift +++ b/PadelClub/Data/Club.swift @@ -32,7 +32,9 @@ class Club : ModelObject, Storable, Hashable { var zipCode: String? var latitude: Double? var longitude: Double? - + var courtCount: Int? + var courtNames: [String]? = nil + internal init(name: String, acronym: String? = nil, phone: String? = nil, code: String? = nil, address: String? = nil, city: String? = nil, zipCode: String? = nil, latitude: Double? = nil, longitude: Double? = nil) { self.name = name self.acronym = acronym ?? name.acronym() @@ -45,12 +47,7 @@ class Club : ModelObject, Storable, Hashable { self.longitude = longitude } - var tournaments: [Tournament] { - return [] - } - override func deleteDependencies() throws { - try Store.main.deleteDependencies(items: self.tournaments) } enum CodingKeys: String, CodingKey { @@ -64,6 +61,8 @@ class Club : ModelObject, Storable, Hashable { case _zipCode = "zipCode" case _latitude = "latitude" case _longitude = "longitude" + case _courtCount = "courtCount" + case _courtNames = "courtNames" } } diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index f89dd00..f917d67 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -24,8 +24,29 @@ class DataStore: ObservableObject { fileprivate(set) var playerRegistrations: StoredCollection fileprivate(set) var rounds: StoredCollection fileprivate(set) var teamScores: StoredCollection + fileprivate(set) var monthData: StoredCollection fileprivate var _userStorage: OptionalStorage = OptionalStorage(fileName: "user.json") + fileprivate var _appSettingsStorage: MicroStorage = MicroStorage() + + var appSettings: AppSettings { + _appSettingsStorage.item + } + + func updateSettings() { + _appSettingsStorage.update { settings in + settings.lastDataSource = appSettings.lastDataSource + settings.callMessageBody = appSettings.callMessageBody + settings.callDisplayFormat = appSettings.callDisplayFormat + settings.callMessageSignature = appSettings.callMessageSignature + settings.callDisplayEntryFee = appSettings.callDisplayEntryFee + settings.callUseFullCustomMessage = appSettings.callUseFullCustomMessage + settings.matchFormatsDefaultDuration = appSettings.matchFormatsDefaultDuration + settings.bracketMatchFormatPreference = appSettings.bracketMatchFormatPreference + settings.groupStageMatchFormatPreference = appSettings.groupStageMatchFormatPreference + settings.loserBracketMatchFormatPreference = appSettings.loserBracketMatchFormatPreference + } + } var user: User? { return self._userStorage.item @@ -44,16 +65,18 @@ class DataStore: ObservableObject { // store.addMigration(Migration(version: 2)) // store.addMigration(Migration(version: 3)) - self.clubs = store.registerCollection(synchronized: false, indexed: true) - self.tournaments = store.registerCollection(synchronized: false, indexed: true) - self.events = store.registerCollection(synchronized: false, indexed: true) - self.groupStages = store.registerCollection(synchronized: false, indexed: true) - self.teamScores = store.registerCollection(synchronized: false, indexed: true) - self.teamRegistrations = store.registerCollection(synchronized: false, indexed: true) - self.playerRegistrations = store.registerCollection(synchronized: false, indexed: true) - self.rounds = store.registerCollection(synchronized: false, indexed: true) - self.matches = store.registerCollection(synchronized: false, indexed: true) - + let indexed : Bool = false + self.clubs = store.registerCollection(synchronized: false, indexed: indexed) + self.tournaments = store.registerCollection(synchronized: false, indexed: indexed) + self.events = store.registerCollection(synchronized: false, indexed: indexed) + self.groupStages = store.registerCollection(synchronized: false, indexed: indexed) + self.teamScores = store.registerCollection(synchronized: false, indexed: indexed) + self.teamRegistrations = store.registerCollection(synchronized: false, indexed: indexed) + self.playerRegistrations = store.registerCollection(synchronized: false, indexed: indexed) + self.rounds = store.registerCollection(synchronized: false, indexed: indexed) + self.matches = store.registerCollection(synchronized: false, indexed: indexed) + self.monthData = store.registerCollection(synchronized: false, indexed: indexed) + NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidLoad, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidChange, object: nil) } diff --git a/PadelClub/Data/Event.swift b/PadelClub/Data/Event.swift index 07cee7a..c3d7a23 100644 --- a/PadelClub/Data/Event.swift +++ b/PadelClub/Data/Event.swift @@ -22,6 +22,7 @@ class Event: ModelObject, Storable { var groupStageFormat: Int? var roundFormat: Int? var loserRoundFormat: Int? + //var timeslots ? internal init(club: String? = nil, name: String? = nil, courtCount: Int? = nil, tenupId: String? = nil, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil) { self.club = club diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index 7e29cd4..c9391ec 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -203,6 +203,7 @@ enum TypePratique: String, Codable { case beach = "BEACH" case padel = "PADEL" case tennis = "TENNIS" + case pickle = "PICKLE" } // MARK: - CategorieTournoi diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 7378361..e17e819 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -19,6 +19,7 @@ class GroupStage: ModelObject, Storable { var size: Int var format: Int? var startDate: Date? + var name: String? var matchFormat: MatchFormat { get { @@ -255,6 +256,15 @@ class GroupStage: ModelObject, Storable { return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!) } } + + func updateMatchFormat(_ matchFormat: MatchFormat) { + self.matchFormat = matchFormat + let playedMatches = playedMatches() + playedMatches.forEach { match in + match.matchFormat = matchFormat + } + try? DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches) + } override func deleteDependencies() throws { try Store.main.deleteDependencies(items: self._matches()) @@ -269,6 +279,7 @@ extension GroupStage { case _size = "size" case _format = "format" case _startDate = "startDate" + case _name = "name" } } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index ccd4c18..f70800a 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -27,6 +27,7 @@ class Match: ModelObject, Storable { var name: String? var order: Int var disabled: Bool = false + var courtIndex: Int? internal init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, matchFormat: MatchFormat? = nil, court: String? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, broadcasted: Bool = false, name: String? = nil, order: Int = 0) { self.round = round @@ -100,8 +101,8 @@ class Match: ModelObject, Storable { return index * 2 + teamPosition.rawValue == bracketPosition } - func estimatedEndDate() -> Date? { - let minutesToAdd = Double(matchFormat.estimatedDuration) + func estimatedEndDate(_ additionalEstimationDuration: Int) -> Date? { + let minutesToAdd = Double(matchFormat.getEstimatedDuration(additionalEstimationDuration)) return startDate?.addingTimeInterval(minutesToAdd * 60.0) } @@ -315,7 +316,7 @@ class Match: ModelObject, Storable { } } - func courtIndex() -> Int? { + func getCourtIndex() -> Int? { guard let court else { return nil } if let courtIndex = Int(court) { return courtIndex - 1 } return nil @@ -536,6 +537,7 @@ class Match: ModelObject, Storable { case _index = "index" case _format = "format" case _court = "court" + case _courtIndex = "courtIndex" case _servingTeamId = "servingTeamId" case _winningTeamId = "winningTeamId" case _losingTeamId = "losingTeamId" diff --git a/PadelClub/Data/MockData.swift b/PadelClub/Data/MockData.swift index 10bcd26..064e781 100644 --- a/PadelClub/Data/MockData.swift +++ b/PadelClub/Data/MockData.swift @@ -35,17 +35,12 @@ extension Tournament { } static func newEmptyInstance() -> Tournament { - let lastDataSource: String? = UserDefaults.standard.string(forKey: "lastDataSource") - let lastDataSourceMaleUnranked: Int = UserDefaults.standard.integer(forKey: "lastDataSourceMaleUnranked") - let lastDataSourceFemaleUnranked: Int = UserDefaults.standard.integer(forKey: "lastDataSourceFemaleUnranked") - + let lastDataSource: String? = DataStore.shared.appSettings.lastDataSource var _mostRecentDateAvailable: Date? { guard let lastDataSource else { return nil } return URL.importDateFormatter.date(from: lastDataSource) } - let maleUnrankedValue : Int? = lastDataSourceMaleUnranked == 0 ? nil : lastDataSourceMaleUnranked - let femaleUnrankedValue : Int? = lastDataSourceFemaleUnranked == 0 ? nil : lastDataSourceMaleUnranked let rankSourceDate = _mostRecentDateAvailable //todo @@ -55,7 +50,7 @@ extension Tournament { tournament.federalTournamentAge = FederalTournamentAge.mostUsed(tournaments: tournaments) */ - return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior, maleUnrankedValue: maleUnrankedValue, femaleUnrankedValue: femaleUnrankedValue) + return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior) } } diff --git a/PadelClub/Data/MonthData.swift b/PadelClub/Data/MonthData.swift new file mode 100644 index 0000000..0bb8d46 --- /dev/null +++ b/PadelClub/Data/MonthData.swift @@ -0,0 +1,53 @@ +// +// MonthData.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import Foundation +import SwiftUI +import LeStorage + +@Observable +class MonthData : ModelObject, Storable { + + static func resourceName() -> String { return "month-data" } + + private(set) var id: String = Store.randomId() + private(set) var monthKey: String + private(set) var creationDate: Date + + var maleUnrankedValue: Int? = nil + var femaleUnrankedValue: Int? = nil + + init(monthKey: String) { + self.monthKey = monthKey + self.creationDate = Date() + } + + static func calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async { + let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true) + let lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: false) + + await MainActor.run { + if let lastDataSource = DataStore.shared.appSettings.lastDataSource { + let currentMonthData : MonthData = Store.main.filter(isIncluded: { $0.monthKey == lastDataSource }).first ?? MonthData(monthKey: lastDataSource) + currentMonthData.maleUnrankedValue = lastDataSourceMaleUnranked + currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked + try? DataStore.shared.monthData.addOrUpdate(instance: currentMonthData) + } + } + } + + override func deleteDependencies() throws { + } + + enum CodingKeys: String, CodingKey { + case _id = "id" + case _monthKey = "monthKey" + case _creationDate = "creationDate" + case _maleUnrankedValue = "maleUnrankedValue" + case _femaleUnrankedValue = "femaleUnrankedValue" + } +} diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 823e143..4577bd4 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -387,6 +387,15 @@ class Round: ModelObject, Storable { return Store.main.findById(parentRound) } + func updateMatchFormat(_ matchFormat: MatchFormat) { + self.matchFormat = matchFormat + let playedMatches = _matches() + playedMatches.forEach { match in + match.matchFormat = matchFormat + } + try? DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches) + } + override func deleteDependencies() throws { try Store.main.deleteDependencies(items: _matches()) try Store.main.deleteDependencies(items: loserRoundsAndChildren()) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 95791bc..bd44b9b 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -41,14 +41,13 @@ class Tournament : ModelObject, Storable { var qualifiedPerGroupStage: Int var teamsPerGroupStage: Int var entryFee: Double? - var maleUnrankedValue: Int? - var femaleUnrankedValue: Int? var payment: TournamentPayment = .free + var additionalEstimationDuration: Int = 0 @ObservationIgnored var navigationPath: [Screen] = [] - 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? = nil, 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) { + 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? = nil, 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) { self.event = event self.creator = creator self.name = name @@ -77,8 +76,6 @@ class Tournament : ModelObject, Storable { self.qualifiedPerGroupStage = qualifiedPerGroupStage self.teamsPerGroupStage = teamsPerGroupStage self.entryFee = entryFee - self.maleUnrankedValue = maleUnrankedValue - self.femaleUnrankedValue = femaleUnrankedValue self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType } @@ -386,7 +383,7 @@ class Tournament : ModelObject, Storable { _sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending) } - let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + //let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) //print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds]))) return _sortedTeams } @@ -608,22 +605,27 @@ class Tournament : ModelObject, Storable { func updateRank(to newDate: Date?) async throws { guard let newDate else { return } rankSourceDate = newDate - - let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate) - let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate) - + if currentMonthData() == nil { + let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate) + let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate) + await MainActor.run { + let monthData = MonthData(monthKey: URL.importDateFormatter.string(from: newDate)) + monthData.maleUnrankedValue = lastRankMan + monthData.femaleUnrankedValue = lastRankWoman + try? DataStore.shared.monthData.addOrUpdate(instance: monthData) + } + } + + let lastRankMan = currentMonthData()?.maleUnrankedValue + let lastRankWoman = currentMonthData()?.femaleUnrankedValue + try await unsortedPlayers().concurrentForEach { player in let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate }) let sources = dataURLs.map { CSVParser(url: $0) } try await player.updateRank(from: sources, lastRank: (player.sex == 0 ? lastRankWoman : lastRankMan) ?? 0) } - - await MainActor.run { - self.maleUnrankedValue = lastRankMan - self.femaleUnrankedValue = lastRankWoman - } } func missingUnrankedValue() -> Bool { @@ -1035,6 +1037,21 @@ class Tournament : ModelObject, Storable { try Store.main.deleteDependencies(items: self.groupStages()) try Store.main.deleteDependencies(items: self.rounds()) } + + func currentMonthData() -> MonthData? { + guard let rankSourceDate else { return nil } + let dateString = URL.importDateFormatter.string(from: rankSourceDate) + return Store.main.filter(isIncluded: { $0.monthKey == dateString }).first + } + + var maleUnrankedValue: Int? { + currentMonthData()?.maleUnrankedValue + } + + var femaleUnrankedValue: Int? { + currentMonthData()?.femaleUnrankedValue + } + } extension Tournament { @@ -1068,8 +1085,7 @@ extension Tournament { case _qualifiedPerGroupStage = "qualifiedPerGroupStage" case _teamsPerGroupStage = "teamsPerGroupStage" case _entryFee = "entryFee" - case _maleUnrankedValue = "maleUnrankedValue" - case _femaleUnrankedValue = "femaleUnrankedValue" + case _additionalEstimationDuration = "additionalEstimationDuration" } } diff --git a/PadelClub/Data/User.swift b/PadelClub/Data/User.swift index 63a3ac4..1626594 100644 --- a/PadelClub/Data/User.swift +++ b/PadelClub/Data/User.swift @@ -27,12 +27,7 @@ class User: UserBase { var lastName: String var phone: String? var country: String? - var callMessageBody : String? = nil - var callMessageSignature: String? = nil - var callDisplayFormat: Bool = false - var callDisplayEntryFee: Bool = false - var callUseFullCustomMessage: Bool = false - + init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?) { self.username = username self.firstName = firstName @@ -69,12 +64,6 @@ class User: UserBase { case _lastName = "lastName" case _phone = "phone" case _country = "country" - case _callMessageBody = "callMessageBody" - case _callMessageSignature = "callMessageSignature" - case _callDisplayFormat = "callDisplayFormat" - case _callDisplayEntryFee = "callDisplayEntryFee" - case _callUseFullCustomMessage = "callUseFullCustomMessage" - } } diff --git a/PadelClub/Manager/ContactManager.swift b/PadelClub/Manager/ContactManager.swift index 5c381cd..005e736 100644 --- a/PadelClub/Manager/ContactManager.swift +++ b/PadelClub/Manager/ContactManager.swift @@ -34,7 +34,7 @@ extension ContactType { static let defaultSignature = "" static func callingGroupStageCustomMessage(tournament: Tournament?, startDate: Date?, roundLabel: String) -> String { - let tournamentCustomMessage = DataStore.shared.user?.callMessageBody ?? defaultCustomMessage + let tournamentCustomMessage = DataStore.shared.appSettings.callMessageBody ?? defaultCustomMessage let clubName = tournament?.clubName ?? "" var text = tournamentCustomMessage @@ -49,7 +49,7 @@ extension ContactType { text = text.replacingOccurrences(of: "#jour", with: "\(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))") text = text.replacingOccurrences(of: "#horaire", with: "\(date.formatted(Date.FormatStyle().hour().minute()))") - let signature = DataStore.shared.user?.callMessageSignature ?? defaultSignature + let signature = DataStore.shared.appSettings.callMessageSignature ?? defaultSignature text = text.replacingOccurrences(of: "#signature", with: signature) return text @@ -57,7 +57,7 @@ extension ContactType { static func callingGroupStageMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?) -> String { - let useFullCustomMessage = DataStore.shared.user?.callUseFullCustomMessage ?? false + let useFullCustomMessage = DataStore.shared.appSettings.callUseFullCustomMessage ?? false if useFullCustomMessage { return callingGroupStageCustomMessage(tournament: tournament, startDate: startDate, roundLabel: roundLabel) @@ -66,17 +66,17 @@ extension ContactType { let date = startDate ?? tournament?.startDate ?? Date() let clubName = tournament?.clubName ?? "" - let message = DataStore.shared.user?.callMessageBody ?? defaultCustomMessage - let signature = DataStore.shared.user?.callMessageSignature ?? defaultSignature + let message = DataStore.shared.appSettings.callMessageBody ?? defaultCustomMessage + let signature = DataStore.shared.appSettings.callMessageSignature ?? defaultSignature let localizedCalled = "convoqué" + (tournament?.tournamentCategory == .women ? "e" : "") + "s" var formatMessage: String? { - (DataStore.shared.user?.callDisplayFormat ?? false) ? matchFormat?.computedLongLabel.appending(".") : nil + (DataStore.shared.appSettings.callDisplayFormat ?? false) ? matchFormat?.computedLongLabel.appending(".") : nil } var entryFeeMessage: String? { - (DataStore.shared.user?.callDisplayEntryFee ?? false) ? tournament?.entryFeeMessage : nil + (DataStore.shared.appSettings.callDisplayEntryFee ?? false) ? tournament?.entryFeeMessage : nil } var computedMessage: String { diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index 71a5c96..47e5cc1 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -1046,16 +1046,16 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { } } - var estimatedDuration: Int { - if UserDefaults.standard.object(forKey: format) != nil { - return UserDefaults.standard.integer(forKey: format) - } else { - return defaultEstimatedDuration - } + func getEstimatedDuration(_ additionalDuration: Int = 0) -> Int { + estimatedDuration + additionalDuration + } + + private var estimatedDuration: Int { + DataStore.shared.appSettings.matchFormatsDefaultDuration?[self] ?? defaultEstimatedDuration } - func formattedEstimatedDuration() -> String { - Duration.seconds(estimatedDuration * 60).formatted(.units(allowed: [.minutes])) + func formattedEstimatedDuration(_ additionalDuration: Int = 0) -> String { + Duration.seconds((estimatedDuration + additionalDuration) * 60).formatted(.units(allowed: [.minutes])) } func formattedEstimatedBreakDuration() -> String { diff --git a/PadelClub/Manager/SourceFileManager.swift b/PadelClub/Manager/SourceFileManager.swift index 690e930..018a341 100644 --- a/PadelClub/Manager/SourceFileManager.swift +++ b/PadelClub/Manager/SourceFileManager.swift @@ -12,7 +12,7 @@ class SourceFileManager { static let beachPadel = URL(string: "https://beach-padel.app.fft.fr/beachja/index/")! var lastDataSource: String? { - UserDefaults.standard.string(forKey: "lastDataSource") + DataStore.shared.appSettings.lastDataSource } func lastDataSourceDate() -> Date? { diff --git a/PadelClub/ViewModel/AppScreen.swift b/PadelClub/ViewModel/AppScreen.swift new file mode 100644 index 0000000..1eba92f --- /dev/null +++ b/PadelClub/ViewModel/AppScreen.swift @@ -0,0 +1,13 @@ +// +// AppScreen.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import Foundation + +enum AppScreen: CaseIterable, Identifiable { + var id: Self { self } + case matchFormatSettings +} diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index 3337951..6fa4495 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -62,6 +62,7 @@ enum MatchSchedulerOption: Hashable { class MatchScheduler { static let shared = MatchScheduler() + var additionalEstimationDuration : Int = 0 var options: Set = Set(arrayLiteral: .accountUpperBracketBreakTime) var timeDifferenceLimit: Double = 300.0 var loserBracketRotationDifference: Int = 0 @@ -254,7 +255,7 @@ class MatchScheduler { let matchesByCourt = byCourt[court]?.sorted(by: \.startDate!) let lastMatch = matchesByCourt?.last var results = [(String, Date)]() - if let courtFreeDate = lastMatch?.estimatedEndDate() { + if let courtFreeDate = lastMatch?.estimatedEndDate(additionalEstimationDuration) { results.append((court, courtFreeDate)) } return results @@ -276,7 +277,8 @@ class MatchScheduler { _startDate = match.startDate rotationIndex += 1 } - let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.courtIndex() ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.estimatedDuration, minimumBreakTime: match.matchFormat.breakTime.breakTime) + + let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.getCourtIndex() ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: match.matchFormat.breakTime.breakTime) slots.append(timeMatch) } @@ -398,7 +400,7 @@ class MatchScheduler { matchPerRound[first.roundObject!.index] = 1 } } - let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.estimatedDuration, minimumBreakTime: first.matchFormat.breakTime.breakTime) + let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: first.matchFormat.breakTime.breakTime) slots.append(timeMatch) availableMatchs.removeAll(where: { $0.id == first.id }) } else { diff --git a/PadelClub/ViewModel/NavigationViewModel.swift b/PadelClub/ViewModel/NavigationViewModel.swift index 70669d8..1a8467e 100644 --- a/PadelClub/ViewModel/NavigationViewModel.swift +++ b/PadelClub/ViewModel/NavigationViewModel.swift @@ -10,6 +10,7 @@ import SwiftUI @Observable class NavigationViewModel { var path = NavigationPath() + var selectedTab: TabDestination? var agendaDestination: AgendaDestination? = .activity var tournament: Tournament? } diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift index 6d8f309..3187722 100644 --- a/PadelClub/Views/Calling/CallMessageCustomizationView.swift +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -10,18 +10,16 @@ import SwiftUI struct CallMessageCustomizationView: View { @EnvironmentObject var dataStore: DataStore var tournament: Tournament - var user: User @FocusState private var textEditor: Bool @State private var customClubName: String = "" @State private var customCallMessageBody: String = "" @State private var customCallMessageSignature: String = "" - init(tournament: Tournament, user: User) { + init(tournament: Tournament) { self.tournament = tournament - self.user = user - _customCallMessageBody = State(wrappedValue: user.callMessageBody ?? "") - _customCallMessageSignature = State(wrappedValue: user.callMessageSignature ?? "") + _customCallMessageBody = State(wrappedValue: DataStore.shared.appSettings.callMessageBody ?? "") + _customCallMessageSignature = State(wrappedValue: DataStore.shared.appSettings.callMessageSignature ?? "") _customClubName = State(wrappedValue: tournament.clubName ?? "") } @@ -30,11 +28,11 @@ struct CallMessageCustomizationView: View { } var formatMessage: String? { - user.callDisplayFormat ? tournament.matchFormat.computedLongLabel + "." : nil + dataStore.appSettings.callDisplayFormat ? tournament.matchFormat.computedLongLabel + "." : nil } var entryFeeMessage: String? { - user.callDisplayEntryFee ? tournament.entryFeeMessage : nil + dataStore.appSettings.callDisplayEntryFee ? tournament.entryFeeMessage : nil } var computedMessage: String { @@ -47,7 +45,7 @@ struct CallMessageCustomizationView: View { } var body: some View { - @Bindable var user = user + @Bindable var appSettings = dataStore.appSettings List { Section { ZStack { @@ -86,7 +84,7 @@ struct CallMessageCustomizationView: View { } Section { - if user.callUseFullCustomMessage { + if appSettings.callUseFullCustomMessage { Text(self.computedFullCustomMessage()) .contextMenu { Button("Coller dans le presse-papier") { @@ -108,7 +106,7 @@ struct CallMessageCustomizationView: View { Section { LabeledContent { - Toggle(isOn: $user.callUseFullCustomMessage) { + Toggle(isOn: $appSettings.callUseFullCustomMessage) { } } label: { @@ -124,13 +122,13 @@ struct CallMessageCustomizationView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { Menu { - Picker(selection: $user.callDisplayFormat) { + Picker(selection: $appSettings.callDisplayFormat) { Text("Afficher le format").tag(true) Text("Masquer le format").tag(false) } label: { } - Picker(selection: $user.callDisplayEntryFee) { + Picker(selection: $appSettings.callDisplayEntryFee) { Text("Afficher le prix d'inscription").tag(true) Text("Masquer le prix d'inscription").tag(false) } label: { @@ -151,30 +149,30 @@ struct CallMessageCustomizationView: View { } } } - .onChange(of: user.callUseFullCustomMessage) { - if user.callUseFullCustomMessage == false { - user.callMessageBody = ContactType.defaultCustomMessage + .onChange(of: appSettings.callUseFullCustomMessage) { + if appSettings.callUseFullCustomMessage == false { + appSettings.callMessageBody = ContactType.defaultCustomMessage } _save() } .onChange(of: customCallMessageBody) { - user.callMessageBody = customCallMessageBody + appSettings.callMessageBody = customCallMessageBody _save() } .onChange(of: customCallMessageSignature) { - user.callMessageSignature = customCallMessageSignature + appSettings.callMessageSignature = customCallMessageSignature _save() } - .onChange(of: user.callDisplayEntryFee) { + .onChange(of: appSettings.callDisplayEntryFee) { _save() } - .onChange(of: user.callDisplayFormat) { + .onChange(of: appSettings.callDisplayFormat) { _save() } } private func _save() { - try? dataStore.setUser(user) + dataStore.updateSettings() } func computedFullCustomMessage() -> String { diff --git a/PadelClub/Views/Calling/CallSettingsView.swift b/PadelClub/Views/Calling/CallSettingsView.swift index 48d868e..b718ef6 100644 --- a/PadelClub/Views/Calling/CallSettingsView.swift +++ b/PadelClub/Views/Calling/CallSettingsView.swift @@ -14,16 +14,14 @@ struct CallSettingsView: View { var body: some View { List { - if let user = dataStore.user { - Section { - NavigationLink { - CallMessageCustomizationView(tournament: tournament, user: user) - } label: { - Text("Personnaliser le message de convocation") - } + Section { + NavigationLink { + CallMessageCustomizationView(tournament: tournament) + } label: { + Text("Personnaliser le message de convocation") } } - + Section { RowButtonView("Envoyer un message à tout le monde") { diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index f6770ff..f753c68 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -339,6 +339,7 @@ enum Pratique: String, Codable { case beach = "BEACH" case padel = "PADEL" case tennis = "TENNIS" + case pickle = "PICKLE" } // MARK: - ClubMarker diff --git a/PadelClub/Views/ClubView.swift b/PadelClub/Views/ClubView.swift deleted file mode 100644 index 219d105..0000000 --- a/PadelClub/Views/ClubView.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ClubView.swift -// PadelClub -// -// Created by Laurent Morvillier on 06/02/2024. -// - -import SwiftUI - -struct ClubView: View { - - var club: Club - - var body: some View { - List(club.tournaments) { tournament in - Text(tournament.tournamentTitle()) - }.navigationTitle(club.name) - } -} - -#Preview { - ClubView(club: Club(name: "AUC", acronym: "test", address: "")) -} diff --git a/PadelClub/Views/Components/Labels.swift b/PadelClub/Views/Components/Labels.swift index 269c18d..eb1471c 100644 --- a/PadelClub/Views/Components/Labels.swift +++ b/PadelClub/Views/Components/Labels.swift @@ -15,13 +15,13 @@ struct LabelOptions: View { struct LabelStructure: View { var body: some View { - Label("Structure", systemImage: "hammer") + Label("Structure", systemImage: "hammer").labelStyle(.titleOnly) } } struct LabelSettings: View { var body: some View { - Label("Réglages", systemImage: "slider.horizontal.3") + Label("Réglages", systemImage: "slider.horizontal.3").labelStyle(.titleOnly) } } diff --git a/PadelClub/Views/Components/StepperView.swift b/PadelClub/Views/Components/StepperView.swift index 2df2604..197c2b8 100644 --- a/PadelClub/Views/Components/StepperView.swift +++ b/PadelClub/Views/Components/StepperView.swift @@ -13,12 +13,12 @@ struct StepperView: View { var title: String? = nil @Binding var count: Int - + var step: Int = 1 var minimum: Int? = nil var maximum: Int? = nil var body: some View { - VStack(spacing: 0) { + VStack { HStack(spacing: 8) { Button(action: { self._subtract() @@ -74,14 +74,14 @@ struct StepperView: View { if let maximum, self.count + 1 > maximum { return } - self.count += 1 + self.count += step } fileprivate func _subtract() { if let minimum, self.count - 1 < minimum { return } - self.count -= 1 + self.count -= step } } diff --git a/PadelClub/Views/ContentView.swift b/PadelClub/Views/ContentView.swift deleted file mode 100644 index 5d92628..0000000 --- a/PadelClub/Views/ContentView.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// ContentView.swift -// PadelClub -// -// Created by Laurent Morvillier on 02/02/2024. -// - -import SwiftUI -import LeStorage - -struct ContentView: View { - - @StateObject var dataStore = DataStore() - - var body: some View { - NavigationStack { - - VStack { - - List(self.dataStore.clubs) { club in - - NavigationLink { - ClubView(club: club) - } label: { - Text(club.name) - } - } - - Button("add") { - self._add() - } - .padding() - .buttonStyle(.bordered) - } - .toolbar(content: { - ToolbarItem { - NavigationLink { - MainUserView() - .environmentObject(self.dataStore) - } label: { - Image(systemName: "person.circle.fill") - } - } - - ToolbarItem { - NavigationLink { - SubscriptionView() - } label: { - Image(systemName: "tennisball.circle.fill") - } - } - }) - .navigationTitle("Home") - - } - } - - func _add() { -// let id = (0...1000000).randomElement()! -// let club: Club = Club(name: "test\(id)", address: "some address") -// self.dataStore.clubs.addOrUpdate(instance: club) - -// for _ in 0...20 { -// var clubs: [Club] = [] -// for _ in 0...20 { -// let id = (0...1000000).randomElement()! -// let club: Club = Club(name: "test\(id)", acronym: "test", address: "some address") -// clubs.append(club) -// } -// do { -// try self.dataStore.clubs.append(contentOfs: clubs) -// } catch { -// Logger.error(error) -// } -// } - } - -} - -#Preview { - ContentView() -} diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index 4665e0b..d6e06f1 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -8,6 +8,7 @@ import SwiftUI struct MatchDateView: View { + @Environment(Tournament.self) var tournament: Tournament @EnvironmentObject var dataStore: DataStore var match: Match var showPrefix: Bool = false @@ -31,8 +32,9 @@ struct MatchDateView: View { save() } } else { - Button("Décaler de \(match.matchFormat.estimatedDuration) minutes") { - match.startDate = match.startDate?.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60.0) + let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + Button("Décaler de \(estimatedDuration) minutes") { + match.startDate = match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0) match.endDate = nil save() } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index d12d91e..fceeee2 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -9,6 +9,7 @@ import SwiftUI struct MatchDetailView: View { @EnvironmentObject var dataStore: DataStore + @Environment(Tournament.self) var tournament: Tournament @Environment(\.dismiss) var dismiss let matchViewStyle: MatchViewStyle @@ -320,14 +321,15 @@ struct MatchDetailView: View { Section { if match.hasEnded() == false { + let rotationDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) Picker(selection: $startDateSetup) { if match.isReady() { Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) Text("Tout de suite").tag(MatchDateSetup.now) } - Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-match.matchFormat.estimatedDuration)) - Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration)) + Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-rotationDuration)) + Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(rotationDuration)) Text("À").tag(MatchDateSetup.customDate) } label: { Text("Horaire") diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 3aec8bf..450ec45 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -19,7 +19,6 @@ struct ActivityView: View { @State private var viewStyle: AgendaDestination.ViewStyle = .list @State private var federalTournaments: [FederalTournament] = [] @State private var isGatheringFederalTournaments: Bool = false - @Binding var selectedTab: TabDestination? @State private var error: Error? var runningTournaments: [FederalTournamentHolder] { @@ -257,7 +256,7 @@ struct ActivityView: View { Text("Pour voir vos tournois tenup ici, indiquez vos clubs préférés.") } actions: { RowButtonView("Choisir mes clubs préférés") { - selectedTab = .umpire + navigation.selectedTab = .umpire } } } else { @@ -276,6 +275,5 @@ struct ActivityView: View { } #Preview { - ActivityView(selectedTab: .constant(.activity)) - .environmentObject(DataStore.shared) + ActivityView() } diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 3e9384a..55cec3a 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -6,18 +6,20 @@ // import SwiftUI +import LeStorage struct MainView: View { @StateObject var dataStore = DataStore.shared @AppStorage("importingFiles") var importingFiles: Bool = false - + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + @State private var checkingFilesAttempt: Int = 0 @State private var checkingFiles: Bool = false - @AppStorage("lastDataSource") var lastDataSource: String? - @AppStorage("lastDataSourceMaleUnranked") var lastDataSourceMaleUnranked: Int? - @AppStorage("lastDataSourceFemaleUnranked") var lastDataSourceFemaleUnranked: Int? - + var lastDataSource: String? { + dataStore.appSettings.lastDataSource + } + @Environment(\.managedObjectContext) private var viewContext @FetchRequest( @@ -25,10 +27,10 @@ struct MainView: View { animation: .default) private var players: FetchedResults - @State private var selectedTab: TabDestination? var body: some View { - TabView(selection: $selectedTab) { - ActivityView(selectedTab: $selectedTab) + @Bindable var navigation = navigation + TabView(selection: $navigation.selectedTab) { + ActivityView() .tabItem(for: .activity) TournamentOrganizerView() .tabItem(for: .tournamentOrganizer) @@ -87,7 +89,7 @@ struct MainView: View { } private func _checkSourceFileAvailability() async { - + print(dataStore.appSettings.lastDataSource) print("check internet") print("check files on internet") print("check if any files on internet are more recent than here") @@ -104,21 +106,18 @@ struct MainView: View { private func _startImporting() { importingFiles = true Task { - lastDataSource = await FileImportManager.shared.importDataFromFFT() + let lastDataSource = await FileImportManager.shared.importDataFromFFT() + dataStore.appSettings.lastDataSource = lastDataSource + dataStore.updateSettings() if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) { - await _calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) + await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) } importingFiles = false await _downloadPreviousDate() } } - - private func _calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async { - lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true) - lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: false) - } - + private func _downloadPreviousDate() async { await SourceFileManager.shared.getAllFiles() } diff --git a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift index 8cd43e5..734e89a 100644 --- a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift +++ b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift @@ -24,6 +24,7 @@ struct TournamentOrganizerView: View { ContentUnavailableView("Aucun tournoi sélectionné", systemImage: "rectangle.slash", description: Text("Utilisez l'accès rapide ci-dessous pour éditer un tournoi et passer rapidement d'un tournoi à l'autre.")) .navigationTitle("Gestionnaire de tournois") .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } } Divider() diff --git a/PadelClub/Views/Navigation/PadelClubView.swift b/PadelClub/Views/Navigation/PadelClubView.swift index fd7d081..08c66d5 100644 --- a/PadelClub/Views/Navigation/PadelClubView.swift +++ b/PadelClub/Views/Navigation/PadelClubView.swift @@ -13,7 +13,12 @@ struct PadelClubView: View { @State private var checkingFiles: Bool = false @State private var importingFiles: Bool = false - @AppStorage("lastDataSource") var lastDataSource: String? + @EnvironmentObject var dataStore: DataStore + + var lastDataSource: String? { + dataStore.appSettings.lastDataSource + } + @Environment(\.managedObjectContext) private var viewContext @FetchRequest( @@ -36,17 +41,38 @@ struct PadelClubView: View { List { if let _lastDataSourceDate { Section { - HStack { - VStack(alignment: .leading) { - Text("Classement mensuel utilisé").font(.caption).foregroundStyle(.secondary) - Text(_lastDataSourceDate.monthYearFormatted) - } - Spacer() + LabeledContent { Image(systemName: "checkmark") + } label: { + Text(_lastDataSourceDate.monthYearFormatted) + Text("Classement mensuel utilisé") } } } + let monthData = dataStore.monthData.sorted(by: \.creationDate).reversed() + ForEach(monthData) { monthData in + Section { + LabeledContent { + if let maleUnrankedValue = monthData.maleUnrankedValue { + Text(maleUnrankedValue.formatted()) + } + } label: { + Text("Messieurs") + Text("Rang d'un non classé") + } + LabeledContent { + if let femaleUnrankedValue = monthData.femaleUnrankedValue { + Text(femaleUnrankedValue.formatted()) + } + } label: { + Text("Dames") + Text("Rang d'une non classée") + } + } header: { + Text(monthData.monthKey) + } + } // // if players.isEmpty { // ContentUnavailableView { @@ -60,6 +86,7 @@ struct PadelClubView: View { // } // } } + .headerProminence(.increased) .navigationTitle(TabDestination.padelClub.title) // .task { // await self._checkSourceFileAvailability() @@ -101,7 +128,12 @@ struct PadelClubView: View { private func _startImporting() { importingFiles = true Task { - lastDataSource = await FileImportManager.shared.importDataFromFFT() + let lastDataSource = await FileImportManager.shared.importDataFromFFT() + dataStore.appSettings.lastDataSource = lastDataSource + dataStore.updateSettings() + if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) { + await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) + } importingFiles = false } } diff --git a/PadelClub/Views/Navigation/Toolbox/DurationSettingsView.swift b/PadelClub/Views/Navigation/Toolbox/DurationSettingsView.swift new file mode 100644 index 0000000..1af446c --- /dev/null +++ b/PadelClub/Views/Navigation/Toolbox/DurationSettingsView.swift @@ -0,0 +1,25 @@ +// +// DurationSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct DurationSettingsView: View { + var body: some View { + List { + ForEach(MatchFormat.allCases, id: \.self) { matchFormat in + MatchFormatStorageView(matchFormat: matchFormat) + } + } + .navigationTitle("Durées moyennes") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} + +#Preview { + DurationSettingsView() +} diff --git a/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift b/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift new file mode 100644 index 0000000..3fc5739 --- /dev/null +++ b/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift @@ -0,0 +1,67 @@ +// +// GlobalSettingsView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 16/10/2023. +// + +import SwiftUI + +struct GlobalSettingsView: View { + @EnvironmentObject var dataStore : DataStore + + var body: some View { + @Bindable var appSettings = dataStore.appSettings + List { + Section { + Picker(selection: $appSettings.groupStageMatchFormatPreference) { + Text("Automatique").tag(nil as Int?) + ForEach(MatchFormat.allCases, id: \.self) { format in + Text(format.format).tag(format.rawValue as Int?) + } + } label: { + HStack { + Text("Poule") + Spacer() + } + } + Picker(selection: $appSettings.bracketMatchFormatPreference) { + Text("Automatique").tag(nil as Int?) + ForEach(MatchFormat.allCases, id: \.self) { format in + Text(format.format).tag(format.rawValue as Int?) + } + } label: { + HStack { + Text("Tableau") + Spacer() + } + } + Picker(selection: $appSettings.loserBracketMatchFormatPreference) { + Text("Automatique").tag(nil as Int?) + ForEach(MatchFormat.allCases, id: \.self) { format in + Text(format.format).tag(format.rawValue as Int?) + } + } label: { + HStack { + Text("Match de classement") + Spacer() + } + } + } header: { + Text("Vos formats préférés") + } footer: { + Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") + } + } + .onChange(of: [ + appSettings.bracketMatchFormatPreference, + appSettings.groupStageMatchFormatPreference, + appSettings.loserBracketMatchFormatPreference + ]) { + dataStore.updateSettings() + } + .navigationTitle("Formats par défaut") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} diff --git a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift new file mode 100644 index 0000000..717c440 --- /dev/null +++ b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift @@ -0,0 +1,50 @@ +// +// MatchFormatStorageView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct MatchFormatStorageView: View { + @State private var estimatedDuration: Int + @EnvironmentObject var dataStore: DataStore + + let matchFormat: MatchFormat + + init(matchFormat: MatchFormat) { + self.matchFormat = matchFormat + _estimatedDuration = State(wrappedValue: matchFormat.getEstimatedDuration()) + } + + var body: some View { + Section { + LabeledContent { + StepperView(title: "minutes", count: $estimatedDuration, step: 5) + } label: { + Text("Durée \(matchFormat.format)") + Text(matchFormat.computedShortLabelWithoutPrefix) + } + } footer: { + if estimatedDuration != matchFormat.defaultEstimatedDuration { + HStack { + Spacer() + Button { + self.estimatedDuration = matchFormat.defaultEstimatedDuration + } label: { + Text("remettre la durée par défault") + .underline() + } + .buttonStyle(.borderless) + + } + } + } + .onChange(of: estimatedDuration) { + dataStore.appSettings.saveMatchFormatsDefaultDuration(matchFormat, estimatedDuration: estimatedDuration) + dataStore.updateSettings() + } + } +} + diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index e5bae96..afb41de 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -11,17 +11,34 @@ struct ToolboxView: View { var body: some View { NavigationStack { List { - NavigationLink { - SelectablePlayerListView() - } label: { - Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") + Section { + NavigationLink { + SelectablePlayerListView() + } label: { + Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") + } } - NavigationLink { - RankCalculatorView() - } label: { - Label("Calculateur de points", systemImage: "scalemass") + + Section { + NavigationLink { + RankCalculatorView() + } label: { + Label("Calculateur de points", systemImage: "scalemass") + } } + Section { + NavigationLink { + GlobalSettingsView() + } label: { + Label("Formats de jeu par défaut", systemImage: "megaphone") + } + NavigationLink { + DurationSettingsView() + } label: { + Label("Estimation des durées moyennes", systemImage: "deskclock") + } + } } .navigationTitle(TabDestination.toolbox.title) } diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index da95399..1bcaab2 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -61,7 +61,6 @@ struct UmpireView: View { user.licenceId = nil dataStore.setUser(user) } - .font(.caption) } } @@ -80,7 +79,6 @@ struct UmpireView: View { user.club = nil dataStore.setUser(user) } - .font(.caption) } } } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 601bb67..25f32a5 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -194,11 +194,12 @@ struct PlanningSettingsView: View { dispatch.timedMatches.forEach { matchSchedule in if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { - let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60 + let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60 if let startDate = match.groupStageObject?.startDate { let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd) match.startDate = matchStartDate - lastDate = matchStartDate.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60) + lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60) } match.setCourt(matchSchedule.courtIndex + 1) } diff --git a/PadelClub/Views/Shared/MatchFormatPickerView.swift b/PadelClub/Views/Shared/MatchFormatPickerView.swift index f70c115..1de7981 100644 --- a/PadelClub/Views/Shared/MatchFormatPickerView.swift +++ b/PadelClub/Views/Shared/MatchFormatPickerView.swift @@ -8,6 +8,7 @@ import SwiftUI struct MatchFormatPickerView: View { + @Environment(Tournament.self) var tournament: Tournament let headerLabel: String @Binding var matchFormat: MatchFormat @State private var isExpanded: Bool = false @@ -40,7 +41,7 @@ struct MatchFormatPickerView: View { Text(matchFormat.format).font(.largeTitle) Spacer() VStack(alignment: .trailing) { - Text("~" + matchFormat.formattedEstimatedDuration()) + Text("~" + matchFormat.formattedEstimatedDuration(tournament.additionalEstimationDuration)) Text(matchFormat.formattedEstimatedBreakDuration() + " de pause").foregroundStyle(.secondary).font(.subheadline) } } diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index e795259..7b72993 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -17,10 +17,15 @@ struct SelectablePlayerListView: View { let allowSelection: Int let playerSelectionAction: PlayerSelectionAction? let contentUnavailableAction: ContentUnavailableAction? - + + @EnvironmentObject var dataStore: DataStore @StateObject private var searchViewModel: SearchViewModel @Environment(\.dismiss) var dismiss - @AppStorage("lastDataSource") var lastDataSource: String? + + var lastDataSource: String? { + dataStore.appSettings.lastDataSource + } + @AppStorage("importingFiles") var importingFiles: Bool = false @State private var searchText: String = "" diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift index 3ac8a52..0b5e2aa 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift @@ -57,7 +57,7 @@ struct TournamentClubSettingsView: View { TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount, max: 100) } } - .onDisappear { + .onChange(of: tournament.courtCount) { try? dataStore.tournaments.addOrUpdate(instance: tournament) } } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 094596a..0da60b0 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -58,7 +58,6 @@ struct TournamentGeneralSettingsView: View { } .focused($textFieldIsFocus) .scrollDismissesKeyboard(.immediately) - .navigationTitle("Réglages") .toolbarBackground(.visible, for: .navigationBar) .toolbar { ToolbarItem(placement: .keyboard) { @@ -67,9 +66,28 @@ struct TournamentGeneralSettingsView: View { } } } - .onDisappear { - try? dataStore.tournaments.addOrUpdate(instance: tournament) + .onChange(of: tournament.startDate) { + _save() } + .onChange(of: tournament.entryFee) { + _save() + } + .onChange(of: tournament.name) { + _save() + } + .onChange(of: [ + tournament.dayDuration, + tournament.federalCategory, + tournament.federalLevelCategory, + tournament.federalAgeCategory, + tournament.groupStageSortMode, + ]) { + _save() + } + } + + private func _save() { + try? dataStore.tournaments.addOrUpdate(instance: tournament) } } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift index c477c54..76c9dce 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift @@ -7,11 +7,97 @@ import SwiftUI -struct TournamentMatchFormatsSettingsView: View { +struct TournamentMatchFormatsSettingsView: View { + @Environment(NavigationViewModel.self) var navigation: NavigationViewModel + @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore + @State private var confirmUpdate: Bool = false + @State private var updateCompleted: Bool = false var body: some View { + @Bindable var tournament = tournament List { + if confirmUpdate { + RowButtonView("Modifier les matchs existants", role: .destructive) { + _updateAllFormat() + } + } + TournamentFormatSelectionView() + + Section { + LabeledContent { + StepperView(title: "minutes", count: $tournament.additionalEstimationDuration, step: 5) + } label: { + Text("Modifier les durées moyennes") + } + } footer: { + Text("Cette valeur est rajoutée ou soustraite aux valeurs par défaut. Par exemple, cela peut aider à mieux planifier un tournoi débutant ou jeune.") + } + + Section { + NavigationLink { + DurationSettingsView() + } label: { + Label("Estimation des durées moyennes", systemImage: "deskclock") + } + } } + .onChange(of: [tournament.roundFormat, + tournament.groupStageFormat, + tournament.loserRoundFormat, + ]) { + _save() + _confirmOrSave() + } + .onChange(of: tournament.additionalEstimationDuration) { + _save() + } + .onChange(of: dataStore.appSettings.matchFormatsDefaultDuration) { + _confirmOrSave() + } + .overlay(alignment: .bottom) { + if updateCompleted { + Label("Formats mis à jour", systemImage: "checkmark.circle.fill") + .toastFormatted() + .deferredRendering(for: .seconds(2)) + } + } + } + + private func _confirmOrSave() { + switch tournament.state() { + case .initial: + break + case .build: + confirmUpdate = true + } + } + + private func _updateAllFormat() { + updateCompleted = false + let groupStages = tournament.groupStages() + groupStages.forEach { groupStage in + groupStage.updateMatchFormat(tournament.groupStageMatchFormat) + } + + let allRounds = tournament.allRounds() + allRounds.forEach { round in + if round.isLoserBracket() { + round.updateMatchFormat(tournament.loserBracketMatchFormat) + } else { + round.updateMatchFormat(tournament.matchFormat) + } + } + try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages) + try? dataStore.rounds.addOrUpdate(contentOfs: allRounds) + + confirmUpdate = false + updateCompleted = true + + } + + private func _save() { + try? dataStore.tournaments.addOrUpdate(instance: tournament) } } diff --git a/PadelClub/Views/Tournament/TournamentInitView.swift b/PadelClub/Views/Tournament/TournamentInitView.swift index 8e25141..e6d3f55 100644 --- a/PadelClub/Views/Tournament/TournamentInitView.swift +++ b/PadelClub/Views/Tournament/TournamentInitView.swift @@ -16,6 +16,7 @@ struct TournamentInitView: View { NavigationLink(value: Screen.settings) { LabeledContent { Text(tournament.settingsDescriptionLocalizedLabel()) + .tint(.master) } label: { LabelSettings() } @@ -28,6 +29,7 @@ struct TournamentInitView: View { NavigationLink(value: Screen.structure) { LabeledContent { Text(tournament.structureDescriptionLocalizedLabel()) + .tint(.master) } label: { LabelStructure() } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index c4cfda0..f13aafb 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -11,7 +11,10 @@ struct TournamentView: View { @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament var presentationContext: PresentationContext = .agenda - @AppStorage("lastDataSource") var lastDataSource: String? + + var lastDataSource: String? { + dataStore.appSettings.lastDataSource + } var _lastDataSourceDate: Date? { guard let lastDataSource else { return nil } @@ -91,6 +94,7 @@ struct TournamentView: View { .environment(tournament) }) .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) .toolbar { ToolbarItem(placement: .principal) { VStack { From df56b384e00a0f10f94221eafee878c5b7c43710 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 19 Apr 2024 23:17:55 +0200 Subject: [PATCH 09/11] add premise of court availability fix issue with scheduler fix issue on seeding add the ability to export / import quickly a list of players from padel club export --- PadelClub.xcodeproj/project.pbxproj | 8 + PadelClub/Data/Federal/FederalPlayer.swift | 24 ++ PadelClub/Data/Match.swift | 10 +- PadelClub/Data/MockData.swift | 13 +- PadelClub/Data/PlayerRegistration.swift | 4 +- PadelClub/Data/Tournament.swift | 35 ++- PadelClub/Extensions/Date+Extensions.swift | 19 ++ PadelClub/Manager/FileImportManager.swift | 260 +++++++++++------- PadelClub/Manager/PadelRule.swift | 87 +++--- PadelClub/ViewModel/DateInterval.swift | 32 +++ PadelClub/ViewModel/MatchScheduler.swift | 104 ++++++- PadelClub/ViewModel/SearchViewModel.swift | 2 + PadelClub/ViewModel/SeedInterval.swift | 22 +- PadelClub/Views/Event/EventCreationView.swift | 1 + PadelClub/Views/Match/MatchDetailView.swift | 3 +- .../CourtAvailabilitySettingsView.swift | 157 +++++++++++ .../Views/Planning/PlanningSettingsView.swift | 23 +- PadelClub/Views/Planning/PlanningView.swift | 3 +- .../Planning/RoundScheduleEditorView.swift | 16 +- .../Components/EditablePlayerView.swift | 4 + .../Views/Shared/ImportedPlayerView.swift | 16 +- .../Shared/SelectablePlayerListView.swift | 15 +- .../Views/Tournament/FileImportView.swift | 15 +- .../Screen/InscriptionManagerView.swift | 2 +- .../Shared/TournamentCellView.swift | 6 +- 25 files changed, 680 insertions(+), 201 deletions(-) create mode 100644 PadelClub/ViewModel/DateInterval.swift create mode 100644 PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index a5b55a7..3bd322d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -231,6 +231,8 @@ FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */; }; FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */; }; FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; }; + FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E02BD2A9B600A33B06 /* DateInterval.swift */; }; + FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */; }; FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */; }; FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; }; FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; }; @@ -518,6 +520,8 @@ FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MySortDescriptor.swift; sourceTree = ""; }; FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = ""; }; + FFF116E02BD2A9B600A33B06 /* DateInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateInterval.swift; sourceTree = ""; }; + FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourtAvailabilitySettingsView.swift; sourceTree = ""; }; FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduleEditorView.swift; sourceTree = ""; }; FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = ""; }; FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = ""; }; @@ -955,6 +959,7 @@ FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */, FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */, + FFF116E02BD2A9B600A33B06 /* DateInterval.swift */, ); path = ViewModel; sourceTree = ""; @@ -1168,6 +1173,7 @@ FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */, FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */, FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */, + FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */, FF1162882BD0523B000C4809 /* Components */, ); path = Planning; @@ -1433,6 +1439,7 @@ FF92680D2BCEE5EA0080F940 /* NetworkMonitor.swift in Sources */, FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */, FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */, + FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */, FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */, C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, @@ -1464,6 +1471,7 @@ FF11627A2BCF8109000C4809 /* CallMessageCustomizationView.swift in Sources */, FF025ADB2BD0C2D000A86CF8 /* MatchTeamDetailView.swift in Sources */, FF5DA1952BB927E800A33061 /* GenericDestinationPickerView.swift in Sources */, + FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */, FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */, C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */, FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */, diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift index 32a2010..091421b 100644 --- a/PadelClub/Data/Federal/FederalPlayer.swift +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -22,6 +22,7 @@ protocol PlayerHolder { var ligueName: String? { get } var assimilation: String? { get } var computedAge: Int? { get } + func getAssimilatedAsMaleRank() -> Int? } extension PlayerHolder { @@ -30,8 +31,31 @@ extension PlayerHolder { } } +fileprivate extension Int { + var femaleInMaleAssimilation: Int { + self + femaleInMaleAssimilationAddition + } + + var femaleInMaleAssimilationAddition: Int { + switch self { + 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 ImportedPlayer: PlayerHolder { + func getAssimilatedAsMaleRank() -> Int? { + guard male == false else { return nil } + return getRank()?.femaleInMaleAssimilation + } + var computedAge: Int? { nil } var tournamentPlayed: Int? { diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index f70800a..43c74aa 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -184,7 +184,15 @@ class Match: ModelObject, Storable { } func next() -> Match? { - Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first + Store.main.filter(isIncluded: { $0.round == round && $0.index > index }).sorted(by: \.index).first + } + + func getDuration() -> Int { + if let tournament = currentTournament() { + matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + } else { + matchFormat.getEstimatedDuration() + } } func roundTitle() -> String? { diff --git a/PadelClub/Data/MockData.swift b/PadelClub/Data/MockData.swift index 064e781..7cad2cb 100644 --- a/PadelClub/Data/MockData.swift +++ b/PadelClub/Data/MockData.swift @@ -42,15 +42,12 @@ extension Tournament { } let rankSourceDate = _mostRecentDateAvailable + let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil }.sorted(by: \.startDate).reversed() + let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments) + let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments) + let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments) - //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) + return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge) } } diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index e912c37..5f06bf3 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -343,7 +343,6 @@ class PlayerRegistration: ModelObject, Storable { return 15000 } } - } extension PlayerRegistration: Hashable { @@ -357,6 +356,9 @@ extension PlayerRegistration: Hashable { } extension PlayerRegistration: PlayerHolder { + func getAssimilatedAsMaleRank() -> Int? { + nil + } func getFirstName() -> String { firstName diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index bd44b9b..00bf56f 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -44,6 +44,8 @@ class Tournament : ModelObject, Storable { var payment: TournamentPayment = .free var additionalEstimationDuration: Int = 0 + var courtsUnavailability: [Int: [DateInterval]]? = nil + @ObservationIgnored var navigationPath: [Screen] = [] @@ -257,12 +259,16 @@ class Tournament : ModelObject, Storable { let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) - if availableSeeds.count == availableSeedSpot.count { + if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.dimension == availableSeeds.count { return availableSeedGroup - } else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) { + } else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) && availableSeedGroup.dimension == availableSeedOpponentSpot.count { return availableSeedGroup - } else if let chunk = availableSeedGroup.chunk() { - return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk) + } else if let chunks = availableSeedGroup.chunks() { + if let chunk = chunks.first(where: { seedInterval in + seedInterval.first >= self.seededTeams().count + }) { + return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk) + } } } @@ -500,7 +506,9 @@ class Tournament : ModelObject, Storable { 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)) }) + return players.filter({ + ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true)) + }) } func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? { @@ -515,7 +523,7 @@ class Tournament : ModelObject, Storable { previousTeam.updatePlayers(team.players) teamsToImport.append(previousTeam) } else { - let newTeam = addTeam(team.players) + let newTeam = addTeam(team.players, registrationDate: team.registrationDate) teamsToImport.append(newTeam) } } @@ -905,8 +913,8 @@ class Tournament : ModelObject, Storable { selectedSortedTeams().firstIndex(where: { $0.id == team.id }) } - func addTeam(_ players: Set) -> TeamRegistration { - let team = TeamRegistration(tournament: id, registrationDate: Date()) + func addTeam(_ players: Set, registrationDate: Date? = nil) -> TeamRegistration { + let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date()) team.tournamentCategory = tournamentCategory team.setWeight(from: Array(players)) players.forEach { player in @@ -1005,7 +1013,14 @@ class Tournament : ModelObject, Storable { return groupStageMatchFormat } } - + + func setupFederalSettings() { + teamSorting = tournamentLevel.defaultTeamSortingType + groupStageMatchFormat = groupStageSmartMatchFormat() + loserBracketMatchFormat = loserBracketSmartMatchFormat(1) + matchFormat = roundSmartMatchFormat(1) + } + func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { let format = tournamentLevel.federalFormatForBracketRound(roundIndex) if matchFormat.rank > format.rank { @@ -1129,6 +1144,4 @@ extension Tournament: TournamentBuildHolder { var age: FederalTournamentAge { federalTournamentAge } - - } diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index 835302e..bac5698 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -89,6 +89,10 @@ extension Date { } } + func atBeginningOfDay(hourInt: Int = 9) -> Date { + Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)! + } + static var firstDayOfWeek = Calendar.current.firstWeekday static var capitalizedFirstLettersOfWeekdays: [String] { let calendar = Calendar.current @@ -180,9 +184,15 @@ extension Date { var dayInt: Int { Calendar.current.component(.day, from: self) } + var startOfDay: Date { Calendar.current.startOfDay(for: self) } + + func endOfDay() -> Date { + let calendar = Calendar.current + return calendar.date(bySettingHour: 23, minute: 59, second: 59, of: self)! + } } extension Date { @@ -191,3 +201,12 @@ extension Date { } } +extension Date { + func localizedTime() -> String { + self.formatted(.dateTime.hour().minute()) + } + + func localizedDay() -> String { + self.formatted(.dateTime.weekday(.wide).day()) + } +} diff --git a/PadelClub/Manager/FileImportManager.swift b/PadelClub/Manager/FileImportManager.swift index 3810e7e..b2d6ca5 100644 --- a/PadelClub/Manager/FileImportManager.swift +++ b/PadelClub/Manager/FileImportManager.swift @@ -44,10 +44,13 @@ class FileImportManager { var id: Self { self } case frenchFederation + case padelClub case unknown var localizedLabel: String { switch self { + case .padelClub: + return "Padel Club" case .frenchFederation: return "FFT" case .unknown: @@ -58,24 +61,20 @@ class FileImportManager { struct TeamHolder: Identifiable { let id: UUID = UUID() - let playerOne: PlayerRegistration - let playerTwo: PlayerRegistration + let players: Set let weight: Int let tournamentCategory: TournamentCategory let previousTeam: TeamRegistration? - - init(playerOne: PlayerRegistration, playerTwo: PlayerRegistration, tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?) { - self.playerOne = playerOne - self.playerTwo = playerTwo + var registrationDate: Date? = nil + + init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil) { + self.players = Set(players) self.tournamentCategory = tournamentCategory self.previousTeam = previousTeam - self.weight = playerOne.weight + playerTwo.weight - } - - var players: Set { - Set([playerOne, playerTwo]) + self.weight = players.map { $0.weight }.reduce(0,+) + self.registrationDate = registrationDate } - + func index(in teams: [TeamHolder]) -> Int? { teams.firstIndex(where: { $0.id == id }) } @@ -100,6 +99,60 @@ class FileImportManager { static let FFT_ASSIMILATION_WOMAN_IN_MAN = "A calculer selon la pondération en vigueur" func createTeams(from fileContent: String, tournament: Tournament, fileProvider: FileProvider = .frenchFederation) async -> [TeamHolder] { + + switch fileProvider { + case .frenchFederation: + return await _getFederalTeams(from: fileContent, tournament: tournament) + case .padelClub: + return await _getPadelClubTeams(from: fileContent, tournament: tournament) + case .unknown: + return await _getPadelBusinessLeagueTeams(from: fileContent, tournament: tournament) + } + } + + func importDataFromFFT() async -> String? { + if let importingDate = SourceFileManager.shared.mostRecentDateAvailable { + for source in SourceFile.allCases { + for fileURL in source.currentURLs { + let p = readCSV(inputFile: fileURL) + await importingChunkOfPlayers(p, importingDate: importingDate) + } + } + return URL.importDateFormatter.string(from: importingDate) + } + return nil + } + + + func readCSV(inputFile: URL) -> [FederalPlayer] { + do { + let fileContent = try String(contentsOf: inputFile) + return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData) + } catch { + print("error: \(error)") // to do deal with errors + } + return [] + } + + func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] { + let lines = fileContent.components(separatedBy: "\n") + return lines.compactMap { line in + if line.components(separatedBy: ";").count < 10 { + } else { + let data = line.components(separatedBy: ";").joined(separator: "\n") + return FederalPlayer(data, isMale: isMale) + } + return nil + } + } + + func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async { + for chunk in players.chunked(into: 1000) { + await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate) + } + } + + private func _getFederalTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { let lines = fileContent.components(separatedBy: "\n") guard let firstLine = lines.first else { return [] } var separator = "," @@ -108,58 +161,7 @@ class FileImportManager { } let headerCount = firstLine.components(separatedBy: separator).count var results: [TeamHolder] = [] - if headerCount == 23 && fileProvider == .unknown { //PBL - let fetchRequest = ImportedPlayer.fetchRequest() - let federalContext = PersistenceController.shared.localContainer.viewContext - - lines.dropFirst().forEach { line in - let data = line.components(separatedBy: separator) - if data.count == 23 { - -// let team = Team(context: context) -// let brand = Brand(context: context) -// brand.title = data[2].trimmed -// brand.qualifier = data[0].trimmed -// brand.country = data[1].trimmed -// brand.lineOfBusiness = data[3].trimmed -// if brand.lineOfBusiness == "Bâtiment / Immo" { //quick fix -// brand.lineOfBusiness = "Bâtiment / Immo / Transport" -// } -// brand.name = data[4].trimmed -// team.brand = brand -// -// for i in 0...5 { -// let sex = data[i*3+5] -// let lastName = data[i*3+6].trimmed -// let firstName = data[i*3+7].trimmed -// if lastName.isEmpty == false { -// let playerOne = Player(context: context) -// let predicate = NSPredicate(format: "(canonicalLastName matches[cd] %@ OR canonicalLastName matches[cd] %@) AND (canonicalFirstName matches[cd] %@ OR canonicalFirstName matches[cd] %@)", lastName, lastName.removePunctuationAndHyphens, firstName, firstName.removePunctuationAndHyphens) -// fetchRequest.predicate = predicate -// if let playerFound = try? federalContext.fetch(fetchRequest).first { -// playerOne.updateWithImportedPlayer(playerFound) -// } else { -// playerOne.lastName = lastName -// playerOne.firstName = firstName -// playerOne.sex = sex == "H" ? 1 : sex == "F" ? 0 : -1 -// playerOne.currentRank = tournament?.lastRankMan ?? 0 -// } -// team.addToPlayers(playerOne) -// } -// } -// team.category = TournamentCategory.men.importingRawValue -// -// if let players = team.players, players.count > 0 { -// results.append(team) -// } else { -// context.delete(team) -// } - } - } - - - return results - } else if headerCount <= 18 && fileProvider == .frenchFederation { + if headerCount <= 18 { Array(lines.dropFirst()).chunked(into: 2).forEach { teamLines in if teamLines.count == 2 { let dataOne = teamLines[0].replacingOccurrences(of: "\"", with: "").components(separatedBy: separator) @@ -211,13 +213,13 @@ class FileImportManager { playerOne.setWeight(in: tournament) let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown) playerTwo.setWeight(in: tournament) - let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) results.append(team) } } } return results - } else if headerCount > 18 && fileProvider == .frenchFederation { + } else { lines.dropFirst().forEach { line in let data = line.components(separatedBy: separator) if data.count > 18 { @@ -244,7 +246,7 @@ class FileImportManager { case .mix: return 1 } } - + var sexPlayerTwo : Int { switch tournamentCategory { case .men: return 1 @@ -257,56 +259,108 @@ class FileImportManager { playerOne.setWeight(in: tournament) let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown) playerTwo.setWeight(in: tournament) - - let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + + let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) results.append(team) } } return results - } else { - return [] } } - func importDataFromFFT() async -> String? { - if let importingDate = SourceFileManager.shared.mostRecentDateAvailable { - for source in SourceFile.allCases { - for fileURL in source.currentURLs { - let p = readCSV(inputFile: fileURL) - await importingChunkOfPlayers(p, importingDate: importingDate) + private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { + let lines = fileContent.components(separatedBy: "\n\n") + var results: [TeamHolder] = [] + let fetchRequest = ImportedPlayer.fetchRequest() + let federalContext = PersistenceController.shared.localContainer.viewContext + + lines.forEach { team in + let data = team.components(separatedBy: "\n") + let players = team.licencesFound() + fetchRequest.predicate = NSPredicate(format: "license IN %@", players) + let found = try? federalContext.fetch(fetchRequest) + let registeredPlayers = found?.map({ importedPlayer in + let player = PlayerRegistration(importedPlayer: importedPlayer) + player.setWeight(in: tournament) + return player + }) + if let registeredPlayers, registeredPlayers.isEmpty == false { + var registrationDate: Date? { + if let registrationDateData = data[safe:2]?.replacingOccurrences(of: "inscrit le ", with: "") { + return try? Date(registrationDateData, strategy: .dateTime.weekday().day().month().hour().minute()) + } + return nil } + let team = TeamHolder(players: registeredPlayers, tournamentCategory: tournament.tournamentCategory, previousTeam: tournament.findTeam(registeredPlayers), registrationDate: registrationDate) + results.append(team) } - return URL.importDateFormatter.string(from: importingDate) - } - return nil - } - - - func readCSV(inputFile: URL) -> [FederalPlayer] { - do { - let fileContent = try String(contentsOf: inputFile) - return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData) - } catch { - print("error: \(error)") // to do deal with errors } - return [] + + return results } - func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] { + private func _getPadelBusinessLeagueTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { let lines = fileContent.components(separatedBy: "\n") - return lines.compactMap { line in - if line.components(separatedBy: ";").count < 10 { - } else { - let data = line.components(separatedBy: ";").joined(separator: "\n") - return FederalPlayer(data, isMale: isMale) - } - return nil + guard let firstLine = lines.first else { return [] } + var separator = "," + if firstLine.contains(";") { + separator = ";" } - } - - func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async { - for chunk in players.chunked(into: 1000) { - await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate) + let headerCount = firstLine.components(separatedBy: separator).count + var results: [TeamHolder] = [] + if headerCount == 23 { + //todo + let fetchRequest = ImportedPlayer.fetchRequest() + let federalContext = PersistenceController.shared.localContainer.viewContext + + lines.dropFirst().forEach { line in + let data = line.components(separatedBy: separator) + if data.count == 23 { + +// let team = Team(context: context) +// let brand = Brand(context: context) +// brand.title = data[2].trimmed +// brand.qualifier = data[0].trimmed +// brand.country = data[1].trimmed +// brand.lineOfBusiness = data[3].trimmed +// if brand.lineOfBusiness == "Bâtiment / Immo" { //quick fix +// brand.lineOfBusiness = "Bâtiment / Immo / Transport" +// } +// brand.name = data[4].trimmed +// team.brand = brand +// +// for i in 0...5 { +// let sex = data[i*3+5] +// let lastName = data[i*3+6].trimmed +// let firstName = data[i*3+7].trimmed +// if lastName.isEmpty == false { +// let playerOne = Player(context: context) +// let predicate = NSPredicate(format: "(canonicalLastName matches[cd] %@ OR canonicalLastName matches[cd] %@) AND (canonicalFirstName matches[cd] %@ OR canonicalFirstName matches[cd] %@)", lastName, lastName.removePunctuationAndHyphens, firstName, firstName.removePunctuationAndHyphens) +// fetchRequest.predicate = predicate +// if let playerFound = try? federalContext.fetch(fetchRequest).first { +// playerOne.updateWithImportedPlayer(playerFound) +// } else { +// playerOne.lastName = lastName +// playerOne.firstName = firstName +// playerOne.sex = sex == "H" ? 1 : sex == "F" ? 0 : -1 +// playerOne.currentRank = tournament?.lastRankMan ?? 0 +// } +// team.addToPlayers(playerOne) +// } +// } +// team.category = TournamentCategory.men.importingRawValue +// +// if let players = team.players, players.count > 0 { +// results.append(team) +// } else { +// context.delete(team) +// } + } + } + + + return results } + return [] } } diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index 47e5cc1..ed94063 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -159,20 +159,18 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { case a45 = 450 case a55 = 550 - static func mostRecent(tournaments: [Tournament] = []) -> Self { - .senior -// return tournaments.first?.federalTournamentAge ?? .a11_12 + static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { + return tournaments.first?.federalTournamentAge ?? .senior } - static func mostUsed(tournaments: [Tournament] = []) -> Self { -// let countedSet = NSCountedSet(array: tournaments.map { $0.federalTournamentAge }) -// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } -// if mostFrequent != nil { -// return mostFrequent as! FederalTournamentAge -// } else { -// return mostRecent(tournaments: tournaments) -// } - .senior + static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { + let countedSet = NSCountedSet(array: tournaments.map { $0.federalTournamentAge }) + let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } + if mostFrequent != nil { + return mostFrequent as! FederalTournamentAge + } else { + return mostRecent(inTournaments: tournaments) + } } var id: Int { self.rawValue } @@ -236,22 +234,20 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { case p1500 = 1500 case p2000 = 2000 - static func mostRecent(tournaments: [Tournament] = []) -> Self { - //return tournaments.first?.tournamentLevel ?? .p25 - .p100 + static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { + return tournaments.first?.tournamentLevel ?? .p100 } - static func mostUsed(tournaments: [Tournament] = []) -> Self { -// let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentLevel }) -// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } -// if mostFrequent != nil { -// return mostFrequent as! TournamentLevel -// } else { -// return mostRecent(tournaments: tournaments) -// } - .p100 + static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { + let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentLevel }) + let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } + if mostFrequent != nil { + return mostFrequent as! TournamentLevel + } else { + return mostRecent(inTournaments: tournaments) + } } - + var id: Int { self.rawValue } func maximumDuration() -> Double { @@ -631,20 +627,27 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable { } } - static func mostRecent(tournaments: [Tournament] = []) -> Self { - //return tournaments.first?.tournamentCategory ?? .mix - .men + var showFemaleInMaleAssimilation: Bool { + switch self { + case .men: + return true + default: + return false + } + } + + static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { + return tournaments.first?.tournamentCategory ?? .men } - static func mostUsed(tournaments: [Tournament] = []) -> Self { -// let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentCategory }) -// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } -// if mostFrequent != nil { -// return mostFrequent as! TournamentCategory -// } else { -// return mostRecent(tournaments: tournaments) -// } - .men + static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { + let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentCategory }) + let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } + if mostFrequent != nil { + return mostFrequent as! TournamentCategory + } else { + return mostRecent(inTournaments: tournaments) + } } var next: TournamentCategory { @@ -1019,10 +1022,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { } static func defaultFormatForMatchType(_ matchType: MatchType) -> MatchFormat { - if UserDefaults.standard.object(forKey: matchType.rawValue + "MatchFormatPreference") == nil { - return .nineGamesDecisivePoint + switch matchType { + case .bracket: + MatchFormat(rawValue: DataStore.shared.appSettings.bracketMatchFormatPreference) ?? .nineGamesDecisivePoint + case .groupStage: + MatchFormat(rawValue: DataStore.shared.appSettings.groupStageMatchFormatPreference) ?? .nineGamesDecisivePoint + case .loserBracket: + MatchFormat(rawValue: DataStore.shared.appSettings.loserBracketMatchFormatPreference) ?? .nineGamesDecisivePoint } - return MatchFormat(rawValue: UserDefaults.standard.integer(forKey: matchType.rawValue + "MatchFormatPreference")) ?? .nineGamesDecisivePoint } static var allCases: [MatchFormat] { diff --git a/PadelClub/ViewModel/DateInterval.swift b/PadelClub/ViewModel/DateInterval.swift new file mode 100644 index 0000000..58876bf --- /dev/null +++ b/PadelClub/ViewModel/DateInterval.swift @@ -0,0 +1,32 @@ +// +// DateInterval.swift +// PadelClub +// +// Created by Razmig Sarkissian on 19/04/2024. +// + +import Foundation +import LeStorage + +struct DateInterval: Identifiable, Codable { + var id: String = Store.randomId() + + let startDate: Date + let endDate: Date + + var range: Range { + startDate.. Bool { + Calendar.current.isDate(startDate, inSameDayAs: endDate) + } + + func isDateInside(_ date: Date) -> Bool { + date >= startDate && date <= endDate + } + + func isDateOutside(_ date: Date) -> Bool { + date <= startDate && date <= endDate && date >= startDate && date >= endDate + } +} diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index 6fa4495..90ecb2d 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -67,6 +67,7 @@ class MatchScheduler { var timeDifferenceLimit: Double = 300.0 var loserBracketRotationDifference: Int = 0 var upperBracketRotationDifference: Int = 1 + var courtsUnavailability: [Int: [DateInterval]]? = nil func shouldHandleUpperRoundSlice() -> Bool { options.contains(.shouldHandleUpperRoundSlice) @@ -176,7 +177,18 @@ class MatchScheduler { } func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool { - //print(roundObject.roundTitle(), match.matchTitle()) + print(roundObject.roundTitle(), match.matchTitle()) + + if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate { + print("can't start \(targetedStartDate) earlier than \(roundStartDate)") + if targetedStartDate == minimumTargetedEndDate { + minimumTargetedEndDate = roundStartDate + } else { + minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate) + } + return false + } + let previousMatches = roundObject.precedentMatches(ofMatch: match) if previousMatches.isEmpty { return true } @@ -361,33 +373,55 @@ class MatchScheduler { func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]]) { var matchPerRound = [Int: Int]() var minimumTargetedEndDate: Date = rotationStartDate - courts.forEach { courtIndex in - //print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) }) - + print("dispatchCourts", courts.sorted(), rotationStartDate, rotationIndex) + courts.sorted().forEach { courtIndex in + print("trying to find a match for \(courtIndex) in \(rotationIndex)") if let first = availableMatchs.first(where: { match in let roundObject = match.roundObject! + let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration)) + print("courtsUnavailable \(courtsUnavailable)") + if courtIndex >= availableCourts - courtsUnavailable { + return false + } + let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0 if shouldHandleUpperRoundSlice() { let roundMatchesCount = roundObject.playedMatches().count + print("shouldHandleUpperRoundSlice \(roundMatchesCount)") if roundObject.loser == nil && roundMatchesCount > courts.count { - if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { return false } + print("roundMatchesCount \(roundMatchesCount) > \(courts.count)") + if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { + print("return false, \(currentRotationSameRoundMatches) >= \(min(roundMatchesCount / 2, courts.count))") + return false + } } } - if roundObject.loser == nil && roundObject.index > 0, match.indexInRound() == 0, courts.count > 1, let nextMatch = match.next() { + let indexInRound = match.indexInRound() + + print("Upper Round, index > 0, first Match of round \(indexInRound) and more than one court available; looking for next match (same round) \(indexInRound + 1)") + if roundObject.loser == nil && roundObject.index > 0, indexInRound == 0, courts.count > 1, let nextMatch = match.next() { if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { + + print("next match and this match can be played, returning true") + return true } else { + print("next match and this match can not be played at the same time, returning false") return false } } + + print("\(currentRotationSameRoundMatches) modulo \(currentRotationSameRoundMatches%2) same round match is even, index of round is not 0 and upper bracket. If it's not the last court available \(courtIndex) == \(courts.count - 1)") if currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == courts.count - 1 { + print("we return false") return false } + return canBePlayed }) { @@ -409,11 +443,17 @@ class MatchScheduler { } if freeCourtPerRotation[rotationIndex]!.count == availableCourts { + print("no match found to be put in this rotation, check if we can put anything to another date") freeCourtPerRotation[rotationIndex] = [] let courtsUsed = getNextEarliestAvailableDate(from: slots) - let freeCourts = courtsUsed.filter { (courtIndex, availableDate) in - availableDate <= minimumTargetedEndDate - }.sorted(by: \.1).map { $0.0 } + var freeCourts: [Int] = [] + if courtsUsed.isEmpty { + freeCourts = (0.. Int { + let endDate = startDate.addingTimeInterval(Double(duration) * 60) + guard let courtsUnavailability else { return 0 } + let courts = courtsUnavailability.keys + return courts.filter { + courtUnavailable(courtIndex: $0, from: startDate, to: endDate) + }.count + } + func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool { + guard let courtLockedSchedule = courtsUnavailability?[courtIndex] else { return true } + return courtLockedSchedule.anySatisfy({ dateInterval in + dateInterval.isDateInside(startDate) || dateInterval.isDateInside(endDate) + }) + } +} diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index 6e4d80c..e906c6d 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -14,6 +14,8 @@ class SearchViewModel: ObservableObject, Identifiable { var codeClub: String? = nil var clubName: String? = nil var ligueName: String? = nil + var showFemaleInMaleAssimilation: Bool = false + @Published var debouncableText: String = "" @Published var searchText: String = "" @Published var task: DispatchWorkItem? diff --git a/PadelClub/ViewModel/SeedInterval.swift b/PadelClub/ViewModel/SeedInterval.swift index d0a82c0..e9d397c 100644 --- a/PadelClub/ViewModel/SeedInterval.swift +++ b/PadelClub/ViewModel/SeedInterval.swift @@ -15,10 +15,26 @@ struct SeedInterval: Hashable, Comparable { return lhs.first < rhs.first } + var dimension: Int { + (last - first) + } + + func chunks() -> [SeedInterval]? { + if dimension > 3 { + let split = dimension / 2 + let firstHalf = SeedInterval(first: first, last: first + split - 1) + let secondHalf = SeedInterval(first: first + split, last: last) + return [firstHalf, secondHalf] + } else { + return nil + } + } + func chunk() -> SeedInterval? { - if (last - first) / 2 > 0 { - if last - (last - first) / 2 > first { - return SeedInterval(first: first, last: last - (last - first) / 2) + if dimension / 2 > 0 { + let halfDimension = last - dimension / 2 + if halfDimension > first { + return SeedInterval(first: first, last: halfDimension - 1) } } return nil diff --git a/PadelClub/Views/Event/EventCreationView.swift b/PadelClub/Views/Event/EventCreationView.swift index c908431..9ee8d7e 100644 --- a/PadelClub/Views/Event/EventCreationView.swift +++ b/PadelClub/Views/Event/EventCreationView.swift @@ -100,6 +100,7 @@ struct EventCreationView: View { tournaments.forEach { tournament in tournament.startDate = startingDate tournament.dayDuration = duration + tournament.setupFederalSettings() } try? dataStore.tournaments.addOrUpdate(contentOfs: tournaments) diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index fceeee2..399f2b3 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -9,7 +9,6 @@ import SwiftUI struct MatchDetailView: View { @EnvironmentObject var dataStore: DataStore - @Environment(Tournament.self) var tournament: Tournament @Environment(\.dismiss) var dismiss let matchViewStyle: MatchViewStyle @@ -321,7 +320,7 @@ struct MatchDetailView: View { Section { if match.hasEnded() == false { - let rotationDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + let rotationDuration = match.getDuration() Picker(selection: $startDateSetup) { if match.isReady() { Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift new file mode 100644 index 0000000..3f34b5b --- /dev/null +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -0,0 +1,157 @@ +// +// CourtAvailabilitySettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 19/04/2024. +// + +import SwiftUI + +struct CourtAvailabilitySettingsView: View { + @Environment(Tournament.self) var tournament: Tournament + @State private var courtsUnavailability: [Int: [DateInterval]] = [Int:[DateInterval]]() + @State private var showingPopover: Bool = false + @State private var courtIndex: Int = 0 + @State private var startDate: Date = Date() + @State private var endDate: Date = Date() + + var body: some View { + List { + let keys = courtsUnavailability.keys.sorted(by: \.self) + ForEach(keys, id: \.self) { key in + if let dates = courtsUnavailability[key] { + Section { + ForEach(dates) { dateInterval in + HStack { + VStack(alignment: .leading, spacing: 0) { + Text(dateInterval.startDate.localizedTime()).font(.largeTitle) + Text(dateInterval.startDate.localizedDay()).font(.caption) + } + Spacer() + Image(systemName: "arrowshape.forward.fill") + .tint(.master) + Spacer() + VStack(alignment: .trailing, spacing: 0) { + Text(dateInterval.endDate.localizedTime()).font(.largeTitle) + Text(dateInterval.endDate.localizedDay()).font(.caption) + } + } + .contextMenu(menuItems: { + Button("dupliquer") { + + } + Button("éditer") { + + } + Button("effacer") { + + } + }) + .swipeActions { + Button(role: .destructive) { + courtsUnavailability[key]?.removeAll(where: { $0.id == dateInterval.id }) + } label: { + LabelDelete() + } + } + } + } header: { + Text("Terrain #\(key + 1)") + } + .headerProminence(.increased) + } + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + showingPopover = true + } label: { + Image(systemName: "plus.circle.fill") + .resizable() + .scaledToFit() + .frame(minHeight: 28) + } + } + } + .onDisappear { + tournament.courtsUnavailability = courtsUnavailability + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Créneaux") + .popover(isPresented: $showingPopover) { + NavigationStack { + Form { + Section { + CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: 3) + } + + Section { + DatePicker("Début", selection: $startDate) + DatePicker("Fin", selection: $endDate) + } footer: { + Button("jour entier") { + startDate = startDate.startOfDay + endDate = endDate.endOfDay() + } + .buttonStyle(.borderless) + .underline() + } + } + .toolbar { + Button("Valider") { + let dateInterval = DateInterval(startDate: startDate, endDate: endDate) + var courtUnavailability = courtsUnavailability[courtIndex] ?? [DateInterval]() + courtUnavailability.append(dateInterval) + courtsUnavailability[courtIndex] = courtUnavailability + showingPopover = false + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Nouveau créneau") + } + .onAppear { + UIDatePicker.appearance().minuteInterval = 5 + } + .onDisappear { + UIDatePicker.appearance().minuteInterval = 1 + } + } + } +} + +struct CourtPicker: View { + let title: String + @Binding var selection: Int + let maxCourt: Int + + var body: some View { + Picker(title, selection: $selection) { + ForEach(0.. 1 { +// ForEach(0.. some View { VStack(alignment: .leading) { ImportedPlayerView(player: player) + HStack { + Text(player.isImported() ? "importé" : "non importé") + Text(player.formattedLicense().isLicenseNumber ? "valide" : "non valide") + } HStack { Menu { if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") { diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index 392c64a..3bfa6e2 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -10,6 +10,7 @@ import SwiftUI struct ImportedPlayerView: View { let player: PlayerHolder var index: Int? = nil + var showFemaleInMaleAssimilation: Bool = false var body: some View { VStack(alignment: .leading) { @@ -39,7 +40,7 @@ struct ImportedPlayerView: View { .font(.title3) if let rank = player.getRank() { Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) - .font(.caption) + .font(.caption) } } @@ -58,6 +59,19 @@ struct ImportedPlayerView: View { } } + if let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank(), showFemaleInMaleAssimilation { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(assimilatedAsMaleRank.formatted()) + VStack(alignment: .leading, spacing: 0) { + Text("équivalence") + Text("messieurs") + } + .font(.caption) + } + Text(")").font(.title3) + } + HStack { Text(player.formattedLicense()) if let computedAge = player.computedAge { diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 7b72993..318d7b9 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -34,12 +34,13 @@ struct SelectablePlayerListView: View { return URL.importDateFormatter.date(from: lastDataSource) } - init(allowSelection: Int = 0, searchField: String? = nil, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { + init(allowSelection: Int = 0, searchField: String? = nil, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, showFemaleInMaleAssimilation: Bool = false, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { self.allowSelection = allowSelection // self.searchText = searchField ?? "" self.playerSelectionAction = playerSelectionAction self.contentUnavailableAction = contentUnavailableAction let searchViewModel = SearchViewModel() + searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation searchViewModel.searchText = searchField ?? "" searchViewModel.isPresented = allowSelection != 0 searchViewModel.user = user @@ -292,7 +293,7 @@ struct MySearchView: View { let array = Array(searchViewModel.selectedPlayers) Section { ForEach(array) { player in - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } .onDelete { indexSet in for index in indexSet { @@ -307,7 +308,7 @@ struct MySearchView: View { } else { Section { ForEach(players, id: \.self) { player in - ImportedPlayerView(player: player, index: nil) + ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } } header: { if players.isEmpty == false { @@ -326,7 +327,7 @@ struct MySearchView: View { Button { searchViewModel.selectedPlayers.insert(player) } label: { - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } .buttonStyle(.plain) } @@ -339,7 +340,7 @@ struct MySearchView: View { } else { Section { ForEach(players) { player in - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } } header: { if players.isEmpty == false { @@ -356,13 +357,13 @@ struct MySearchView: View { Button { searchViewModel.selectedPlayers.insert(player) } label: { - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil) + ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) .contentShape(Rectangle()) } .frame(maxWidth: .infinity) .buttonStyle(.plain) } else { - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } } } header: { diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 9eeb438..39f7751 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -40,6 +40,14 @@ struct FileImportView: View { Label("beach-padel.app.fft.fr", systemImage: "tennisball") } + Picker(selection: $fileProvider) { + ForEach(FileImportManager.FileProvider.allCases) { + Text($0.localizedLabel).tag($0) + } + } label: { + Text("Source du fichier") + } + Button { convertingFile = false isShowing.toggle() @@ -160,7 +168,7 @@ struct FileImportView: View { } } } - .fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText], allowsMultipleSelection: false, onCompletion: { results in + .fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText, .text], allowsMultipleSelection: false, onCompletion: { results in switch results { case .success(let fileurls): @@ -267,8 +275,9 @@ struct FileImportView: View { Section { HStack { VStack(alignment: .leading) { - Text(team.playerOne.playerLabel()) - Text(team.playerTwo.playerLabel()) + ForEach(team.players.sorted(by: \.weight)) { + Text($0.playerLabel()) + } } Spacer() HStack { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index dee982e..52273e7 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -76,7 +76,7 @@ struct InscriptionManagerView: View { selectionSearchField = nil }) { NavigationStack { - SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption()) { players in + SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in selectionSearchField = nil players.forEach { player in let newPlayer = PlayerRegistration(importedPlayer: player) diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index a1b6e89..b7fae0a 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -61,11 +61,15 @@ struct TournamentCellView: View { let event = federalTournament.getEvent() let newTournament = Tournament.newEmptyInstance() newTournament.event = event.id + //todo + //newTournament.umpireMail() + //newTournament.jsonData = jsonData newTournament.tournamentLevel = build.level newTournament.tournamentCategory = build.category newTournament.federalTournamentAge = build.age newTournament.dayDuration = federalTournament.dayDuration - newTournament.startDate = federalTournament.startDate + newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) + newTournament.setupFederalSettings() try? dataStore.tournaments.addOrUpdate(instance: newTournament) } } label: { From 38d05af6dfbc7cad2611658b9440b73feeff9090 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 22 Apr 2024 04:43:51 +0200 Subject: [PATCH 10/11] fix bugs --- PadelClub.xcodeproj/project.pbxproj | 8 +- PadelClub/Data/DataStore.swift | 2 +- PadelClub/Data/Match.swift | 98 ++++++++++--- PadelClub/Data/Round.swift | 74 +++++----- PadelClub/Data/Tournament.swift | 19 ++- PadelClub/ViewModel/SeedInterval.swift | 20 +-- .../Match/Components/PlayerBlockView.swift | 3 - PadelClub/Views/Match/MatchRowView.swift | 33 +++++ PadelClub/Views/Round/LoserBracketView.swift | 53 ------- PadelClub/Views/Round/LoserRoundView.swift | 72 ++++++++++ PadelClub/Views/Round/LoserRoundsView.swift | 132 +++++++++--------- PadelClub/Views/Round/RoundSettingsView.swift | 10 ++ PadelClub/Views/Round/RoundView.swift | 8 +- 13 files changed, 329 insertions(+), 203 deletions(-) delete mode 100644 PadelClub/Views/Round/LoserBracketView.swift create mode 100644 PadelClub/Views/Round/LoserRoundView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 3bd322d..067c40c 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -212,7 +212,7 @@ FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */; }; - FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */; }; + FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */; }; FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */; }; FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; }; FFC83D512BB8087E00750834 /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D502BB8087E00750834 /* RoundView.swift */; }; @@ -501,7 +501,7 @@ FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.swift; sourceTree = ""; }; - FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketView.swift; sourceTree = ""; }; + FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundView.swift; sourceTree = ""; }; FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundsView.swift; sourceTree = ""; }; FFC83D4E2BB807D100750834 /* RoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundsView.swift; sourceTree = ""; }; FFC83D502BB8087E00750834 /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = ""; }; @@ -1086,7 +1086,7 @@ FFC83D4E2BB807D100750834 /* RoundsView.swift */, FFC83D502BB8087E00750834 /* RoundView.swift */, FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */, - FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */, + FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */, FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, ); path = Round; @@ -1489,7 +1489,7 @@ FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */, FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */, FF025AE52BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift in Sources */, - FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */, + FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */, FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index f917d67..4d2bf0b 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -65,7 +65,7 @@ class DataStore: ObservableObject { // store.addMigration(Migration(version: 2)) // store.addMigration(Migration(version: 3)) - let indexed : Bool = false + let indexed : Bool = true self.clubs = store.registerCollection(synchronized: false, indexed: indexed) self.tournaments = store.registerCollection(synchronized: false, indexed: indexed) self.events = store.registerCollection(synchronized: false, indexed: indexed) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 43c74aa..833b61b 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -11,6 +11,7 @@ import LeStorage @Observable class Match: ModelObject, Storable { static func resourceName() -> String { "matches" } + var byeState: Bool = false var id: String = Store.randomId() var round: String? @@ -160,27 +161,90 @@ class Match: ModelObject, Storable { _toggleMatchDisableState(false) } + private func _loserMatch() -> Match? { + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) + return roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) + } + private func _toggleLoserMatchDisableState(_ state: Bool) { - if isLoserBracket == false { - let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) - if let loserMatch = roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) { - loserMatch.disabled = state - try? DataStore.shared.matches.addOrUpdate(instance: loserMatch) - loserMatch._toggleLoserMatchDisableState(state) - } - } else { - roundObject?.loserRounds().forEach({ round in - round.handleLoserRoundState() - }) + guard let loserMatch = _loserMatch() else { return } + loserMatch.byeState = state + loserMatch._toggleMatchDisableState(state, forward: true) + + guard let otherMatch = _otherMatch() else { return } + if otherMatch.disabled == state { + loserMatch.byeState = !state } } - fileprivate func _toggleMatchDisableState(_ state: Bool) { + fileprivate func _otherMatch() -> Match? { + guard let round else { return nil } + guard index > 0 else { return nil } + let nextIndex = (index - 1) / 2 + let topMatchIndex = (nextIndex * 2) + 1 + let bottomMatchIndex = (nextIndex + 1) * 2 + let isTopMatch = topMatchIndex + 1 == index + let lookingForIndex = isTopMatch ? topMatchIndex : bottomMatchIndex + return Store.main.filter(isIncluded: { $0.round == round && $0.index == lookingForIndex }).first + } + + private func _forwardMatch(inRound round: Round) -> Match? { + guard let roundObjectNextRound = round.nextRound() else { return nil } + let nextIndex = (index - 1) / 2 + return Store.main.filter(isIncluded: { $0.round == roundObjectNextRound.id && $0.index == nextIndex }).first + } + + func _toggleForwardMatchDisableState(_ state: Bool) { + guard let roundObject else { return } + guard roundObject.loser != nil else { return } + guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return } + guard let next = _otherMatch() else { return } + if next.disabled && byeState == false && next.byeState == false { + forwardMatch.byeState = false + forwardMatch._toggleMatchDisableState(state, forward: true) + } + + if byeState && next.byeState { + print("don't disable forward match") + forwardMatch.byeState = false + forwardMatch._toggleMatchDisableState(false, forward: true) + } else { + forwardMatch.byeState = true + forwardMatch._toggleMatchDisableState(state, forward: true) + } + +// if next.disabled == false { +// forwardMatch.byeState = state +// } + + + +// +// if next.disabled == state { +// if next.byeState != byeState { +// //forwardMatch.byeState = state +// forwardMatch._toggleMatchDisableState(state) +// } else { +// forwardMatch._toggleByeState(state) +// } +// } else { +// } +// forwardMatch._toggleByeState(state) + } + + func _toggleMatchDisableState(_ state: Bool, forward: Bool = false) { + if disabled == state { return } disabled = state - _toggleLoserMatchDisableState(state) - topPreviousRoundMatch()?._toggleMatchDisableState(state) - bottomPreviousRoundMatch()?._toggleMatchDisableState(state) + //byeState = false try? DataStore.shared.matches.addOrUpdate(instance: self) + + _toggleLoserMatchDisableState(state) + if forward { + _toggleForwardMatchDisableState(state) + } else { + topPreviousRoundMatch()?._toggleMatchDisableState(state) + bottomPreviousRoundMatch()?._toggleMatchDisableState(state) + } } func next() -> Match? { @@ -212,14 +276,14 @@ class Match: ModelObject, Storable { func topPreviousRoundMatch() -> Match? { guard let roundObject else { return nil } return Store.main.filter { match in - match.index == topPreviousRoundMatchIndex() && match.round == roundObject.previousRound()?.id + match.index == topPreviousRoundMatchIndex() && match.round != nil && match.round == roundObject.previousRound()?.id }.sorted(by: \.index).first } func bottomPreviousRoundMatch() -> Match? { guard let roundObject else { return nil } return Store.main.filter { match in - match.index == bottomPreviousRoundMatchIndex() && match.round == roundObject.previousRound()?.id + match.index == bottomPreviousRoundMatchIndex() && match.round != nil && match.round == roundObject.previousRound()?.id }.sorted(by: \.index).first } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 4577bd4..66ad6bc 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -209,7 +209,7 @@ class Round: ModelObject, Storable { } func loserRounds(forRoundIndex roundIndex: Int) -> [Round] { - return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.cumulativeMatchCount) + return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.theoryCumulativeMatchCount) } func isDisabled() -> Bool { @@ -268,39 +268,6 @@ class Round: ModelObject, Storable { try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) } - func handleLoserRoundState() { - let _matches = _matches() - _matches.forEach { match in - let previousRound = self.previousRound() - let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: match.index) - var parentMatches = [Match]() - if isLoserBracket(), previousRound == nil, let parentRound = parentRound { - let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) - let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) - parentMatches = [upperBracketTopMatch, upperBracketBottomMatch].compactMap({ $0 }) - } else if let previousRound { - let previousRoundTopMatch : Match? = Store.main.filter { - $0.round == previousRound.id && $0.index == match.topPreviousRoundMatchIndex() - }.first - let previousRoundBottomMatch : Match? = Store.main.filter { - $0.round == previousRound.id && $0.index == match.bottomPreviousRoundMatchIndex() - }.first - parentMatches = [previousRoundTopMatch, previousRoundBottomMatch].compactMap({ $0 }) - } - - if parentMatches.anySatisfy({ $0.disabled }) { - match.disabled = true - } else if parentMatches.allSatisfy({ $0.disabled == false }) { - match.disabled = false - } - } - - try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) - loserRounds().forEach { round in - round.handleLoserRoundState() - } - } - var cumulativeMatchCount: Int { var totalMatches = playedMatches().count if let parent = parentRound { @@ -317,12 +284,45 @@ class Round: ModelObject, Storable { } } - func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String { + + func disabledMatches() -> [Match] { + _matches().filter({ $0.disabled }) + } + + var theoryCumulativeMatchCount: Int { + var totalMatches = RoundRule.numberOfMatches(forRoundIndex: index) + if let parent = parentRound { + totalMatches += parent.theoryCumulativeMatchCount + } + return totalMatches + } + + + func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String { if let parentRound, let initialRound = parentRound.initialRound() { let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count // print("initialRound", initialRound.roundTitle()) - if let initialRoundNextRound = initialRound.nextRound()?.playedMatches() { - return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + (previousRound() ?? parentRound).playedMatches().count).localizedLabel(displayStyle) + if let initialRoundNextRound = initialRound.nextRound() { + let total = initialRoundNextRound.playedMatches().count * 2 + initialRoundNextRound.disabledMatches().count + let previousRoundMatchCount = (previousRound() ?? parentRound).playedMatches().count + let seedInterval = SeedInterval(first: parentMatchCount + total + 1, last: parentMatchCount + total + previousRoundMatchCount).localizedLabel(displayStyle) +// print("seedInterval", seedInterval, parentMatchCount, total, previousRoundMatchCount) + return seedInterval + } + } + return RoundRule.roundName(fromRoundIndex: index) + } + + func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String { + if let parentRound, let initialRound = parentRound.initialRound() { + let parentMatchCount = parentRound.theoryCumulativeMatchCount - RoundRule.numberOfMatches(forRoundIndex: initialRound.index) +// print("initialRound", initialRound.roundTitle()) + if let initialRoundNextRound = initialRound.nextRound() { + let total = initialRoundNextRound.playedMatches().count * 2 + initialRoundNextRound.disabledMatches().count + let previousRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: (previousRound() ?? parentRound).index) + let seedInterval = SeedInterval(first: parentMatchCount + total + 1, last: parentMatchCount + total + previousRoundMatchCount).localizedLabel(displayStyle) +// print("seedInterval", seedInterval, parentMatchCount, total, previousRoundMatchCount) + return seedInterval } } return RoundRule.roundName(fromRoundIndex: index) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 00bf56f..129cdd6 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -259,9 +259,9 @@ class Tournament : ModelObject, Storable { let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) - if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.dimension == availableSeeds.count { + if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count { return availableSeedGroup - } else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) && availableSeedGroup.dimension == availableSeedOpponentSpot.count { + } else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) && availableSeedGroup.count == availableSeedOpponentSpot.count { return availableSeedGroup } else if let chunks = availableSeedGroup.chunks() { if let chunk = chunks.first(where: { seedInterval in @@ -303,8 +303,12 @@ class Tournament : ModelObject, Storable { for (index, seed) in availableSeeds.enumerated() { seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) } - } else if let chunk = seedGroup.chunk() { - setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) + } else if let chunks = seedGroup.chunks() { + if let chunk = chunks.first(where: { seedInterval in + seedInterval.first >= self.seededTeams().count + }) { + setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) + } } } } @@ -343,7 +347,12 @@ class Tournament : ModelObject, Storable { let matches: [Match] = unsortedGroupStages.flatMap { $0._matches() } + allRounds().flatMap { $0._matches() } return matches.filter({ $0.disabled == false }) } - + + func _allMatchesIncludingDisabled() -> [Match] { + let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id } + return unsortedGroupStages.flatMap { $0._matches() } + allRounds().flatMap { $0._matches() } + } + func allRounds() -> [Round] { Store.main.filter { $0.tournament == self.id } } diff --git a/PadelClub/ViewModel/SeedInterval.swift b/PadelClub/ViewModel/SeedInterval.swift index e9d397c..ff140df 100644 --- a/PadelClub/ViewModel/SeedInterval.swift +++ b/PadelClub/ViewModel/SeedInterval.swift @@ -15,7 +15,11 @@ struct SeedInterval: Hashable, Comparable { return lhs.first < rhs.first } - var dimension: Int { + var count: Int { + dimension + 1 + } + + private var dimension: Int { (last - first) } @@ -29,21 +33,9 @@ struct SeedInterval: Hashable, Comparable { return nil } } - - func chunk() -> SeedInterval? { - if dimension / 2 > 0 { - let halfDimension = last - dimension / 2 - if halfDimension > first { - return SeedInterval(first: first, last: halfDimension - 1) - } - } - return nil - } -} -extension SeedInterval { func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { - if last - first < 2 { + if dimension < 2 { return "#\(first) / #\(last)" } else { return "#\(first) à #\(last)" diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index 29b9127..070928e 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -43,9 +43,6 @@ struct PlayerBlockView: View { } private func _defaultLabel() -> String { - if match.upperBracketMatch(teamPosition)?.disabled == true { - return "Bye" - } return teamPosition.localizedLabel() } diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index 0251cbb..be37b09 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -17,6 +17,39 @@ struct MatchRowView: View { if isEditingTournamentSeed.wrappedValue == true && match.isGroupStage() == false && match.isLoserBracket == false { MatchSetupView(match: match) } else { +// MatchSummaryView(match: match, matchViewStyle: matchViewStyle) +// .overlay { +// if match.disabled { +// Image(systemName: "xmark") +// .resizable() +// .scaledToFit() +// .opacity(0.8) +// } +// } +// .contextMenu(menuItems: { +// Text("index: \(match.index)") +// Text("bye state : \(match.byeState)") +// Text("disable state : \(match.disabled)") +// Button("enable") { +// match._toggleMatchDisableState(false) +// } +// Button("disable") { +// match._toggleMatchDisableState(true) +// } +// Button("bye") { +// match.byeState = true +// } +// Button("not bye") { +// match.byeState = false +// } +// Button("solo toggle") { +// match.disabled.toggle() +// } +// Button("toggle fwrd match") { +// match._toggleForwardMatchDisableState(true) +// } +// }) +// NavigationLink { MatchDetailView(match: match, matchViewStyle: matchViewStyle) } label: { diff --git a/PadelClub/Views/Round/LoserBracketView.swift b/PadelClub/Views/Round/LoserBracketView.swift deleted file mode 100644 index 470c033..0000000 --- a/PadelClub/Views/Round/LoserBracketView.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// LoserBracketView.swift -// PadelClub -// -// Created by Razmig Sarkissian on 04/04/2024. -// - -import SwiftUI - -struct LoserBracketView: View { - @EnvironmentObject var dataStore: DataStore - let loserRounds: [Round] - - @ViewBuilder - var body: some View { - if let first = loserRounds.first { - List { - ForEach(loserRounds) { loserRound in - _loserRoundView(loserRound) - let childLoserRounds = loserRound.loserRounds() - if childLoserRounds.isEmpty == false { - let uniqueChildRound = childLoserRounds.first - if childLoserRounds.count == 1, let uniqueChildRound { - _loserRoundView(uniqueChildRound) - } else if let uniqueChildRound { - NavigationLink { - LoserBracketView(loserRounds: childLoserRounds) - } label: { - Text(uniqueChildRound.roundTitle()) - } - } - } - } - } - .navigationTitle(first.roundTitle()) - } - } - - private func _loserRoundView(_ loserRound: Round) -> some View { - Section { - ForEach(loserRound.playedMatches()) { match in - MatchRowView(match: match, matchViewStyle: .standardStyle) - } - } header: { - Text(loserRound.roundTitle()) - } - } -} - -#Preview { - LoserBracketView(loserRounds: [Round.mock()]) - .environmentObject(DataStore.shared) -} diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift new file mode 100644 index 0000000..a744f07 --- /dev/null +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -0,0 +1,72 @@ +// +// LoserRoundView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 04/04/2024. +// + +import SwiftUI + +struct LoserRoundView: View { + @EnvironmentObject var dataStore: DataStore + let loserRounds: [Round] + @State private var isEditingTournamentSeed: Bool = false + + private func _roundDisabled() -> Bool { + loserRounds.allSatisfy({ $0.isDisabled() }) + } + + var body: some View { + List { + if isEditingTournamentSeed == true { + _editingView() + } + + ForEach(loserRounds) { loserRound in + if isEditingTournamentSeed || loserRound.isDisabled() == false { + Section { + let matches = isEditingTournamentSeed ? loserRound.playedMatches() : loserRound.playedMatches().filter({ $0.disabled == false }) + ForEach(matches) { match in + MatchRowView(match: match, matchViewStyle: .standardStyle) + .overlay { + if match.disabled && isEditingTournamentSeed { + Image(systemName: "xmark") + .resizable() + .scaledToFit() + .opacity(0.8) + } + } + .disabled(match.disabled) + } + } header: { + Text(loserRound.roundTitle(.wide)) + } + } + } + } + .headerProminence(.increased) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(isEditingTournamentSeed == true ? "Valider" : "Modifier") { + isEditingTournamentSeed.toggle() + } + } + } + } + + private func _editingView() -> some View { + if _roundDisabled() { + RowButtonView("Jouer ce tour", role: .destructive) { + loserRounds.forEach { round in + round.enableRound() + } + } + } else { + RowButtonView("Ne pas jouer ce tour", role: .destructive) { + loserRounds.forEach { round in + round.disableRound() + } + } + } + } +} diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 8a311a3..9020a1e 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -7,20 +7,79 @@ import SwiftUI +enum LoserRound: Identifiable, Selectable { + case round(turnIndex: Int, rounds: [Round]) + + var id: Int { + switch self { + case .round(let turnIndex, _): + return turnIndex + } + } +} + +extension LoserRound { + func selectionLabel() -> String { + switch self { + case .round(let turnIndex, _): + return "Tour #\(turnIndex + 1)" + } + } + + func badgeValue() -> Int? { + switch self { + case .round(_, let rounds): + return rounds.flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count + } + } + + func badgeImage() -> Badge? { + switch self { + case .round(_, let rounds): + return rounds.allSatisfy({ $0.hasEnded() }) ? .checkmark : nil + } + } +} + + struct LoserRoundsView: View { + @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed var upperBracketRound: Round - @State private var selectedRound: Round? + @State private var selectedRound: LoserRound? let loserRounds: [Round] init(upperBracketRound: Round) { self.upperBracketRound = upperBracketRound self.loserRounds = upperBracketRound.loserRounds() - _selectedRound = State(wrappedValue: upperBracketRound.getActiveLoserRound()) +// _selectedRound = State(wrappedValue: upperBracketRound.getActiveLoserRound()) } + func allDestinations() -> [LoserRound] { +// return loserRounds + if isEditingTournamentSeed.wrappedValue == false { + let loserRounds = loserRounds.filter { loserRound in + upperBracketRound.loserRounds(forRoundIndex: loserRound.index).anySatisfy({ $0.isDisabled() == false }) + } + + var rounds = [LoserRound]() + for (index, round) in loserRounds.enumerated() { + rounds.append(LoserRound.round(turnIndex: index, rounds: upperBracketRound.loserRounds(forRoundIndex: round.index))) + } + + return rounds + } else { + var rounds = [LoserRound]() + for (index, round) in loserRounds.enumerated() { + rounds.append(LoserRound.round(turnIndex: index, rounds: upperBracketRound.loserRounds(forRoundIndex: round.index))) + } + + return rounds + } + } + var body: some View { VStack(spacing: 0) { - GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: loserRounds, nilDestinationIsValid: true) + GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: allDestinations(), nilDestinationIsValid: true) switch selectedRound { case .none: List { @@ -29,72 +88,13 @@ struct LoserRoundsView: View { } } case .some(let selectedRound): - LoserRoundView(loserRounds: upperBracketRound.loserRounds(forRoundIndex: selectedRound.index)) + switch selectedRound { + case .round(_, let rounds): + LoserRoundView(loserRounds: rounds) + } } } .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) } } - -struct LoserRoundView: View { - @EnvironmentObject var dataStore: DataStore - let loserRounds: [Round] - @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed - - private func _roundDisabled() -> Bool { - loserRounds.allSatisfy({ $0.isDisabled() }) - } - - var body: some View { - List { - if isEditingTournamentSeed.wrappedValue == true { - _editingView() - } - - ForEach(loserRounds) { loserRound in - Section { - ForEach(loserRound.playedMatches()) { match in - MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) - .overlay { - if match.disabled { - Image(systemName: "xmark") - .resizable() - .scaledToFit() - .opacity(0.8) - } - } - .disabled(match.disabled) - } - } header: { - Text(loserRound.roundTitle(.wide)) - } - } - } - .headerProminence(.increased) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { - isEditingTournamentSeed.wrappedValue.toggle() - } - } - } - } - - private func _editingView() -> some View { - if _roundDisabled() { - RowButtonView("Jouer ce tour", role: .destructive) { - loserRounds.forEach { round in - round.enableRound() - round.handleLoserRoundState() - } - } - } else { - RowButtonView("Ne pas jouer ce tour", role: .destructive) { - loserRounds.forEach { round in - round.disableRound() - } - } - } - } -} diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 8fd0963..1b535ec 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -14,6 +14,16 @@ struct RoundSettingsView: View { var body: some View { List { + Section { + RowButtonView("Enabled", role: .destructive) { + let allMatches = tournament._allMatchesIncludingDisabled() + allMatches.forEach({ + $0.disabled = false + $0.byeState = false + }) + try? dataStore.matches.addOrUpdate(contentOfs: allMatches) + } + } Section { RowButtonView("Retirer toutes les têtes de séries", role: .destructive) { tournament.unsortedTeams().forEach({ $0.bracketPosition = nil }) diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index c9e0ab2..e78e1f2 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -19,14 +19,16 @@ struct RoundView: View { if isEditingTournamentSeed.wrappedValue == false { let loserRounds = round.loserRounds() - if loserRounds.isEmpty == false, let first = loserRounds.first(where: { $0.isDisabled() == false }) { + //(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue }) + if loserRounds.isEmpty == false, let first = loserRounds.first { + let correspondingLoserRoundTitle = first.correspondingLoserRoundTitle() Section { NavigationLink { LoserRoundsView(upperBracketRound: round) .environment(tournament) - .navigationTitle(first.roundTitle()) + .navigationTitle(correspondingLoserRoundTitle) } label: { - Text(first.roundTitle()) + Text(correspondingLoserRoundTitle) } } } From 34ceea6b2ad859f468dfcb2c9913d83796bf0d1b Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 22 Apr 2024 12:29:30 +0200 Subject: [PATCH 11/11] fix more stuff on loser round --- PadelClub.xcodeproj/project.pbxproj | 2 + PadelClub/Data/Match.swift | 21 ++-- PadelClub/Data/Round.swift | 83 ++++++++------ PadelClub/Data/Tournament.swift | 21 ++-- PadelClub/Manager/PadelRule.swift | 4 +- PadelClub/ViewModel/SeedInterval.swift | 13 +-- .../GenericDestinationPickerView.swift | 42 ++++---- PadelClub/Views/Match/MatchRowView.swift | 5 +- PadelClub/Views/Match/MatchSetupView.swift | 21 ++-- PadelClub/Views/Round/LoserRoundView.swift | 2 +- PadelClub/Views/Round/LoserRoundsView.swift | 101 ++++++++---------- PadelClub/Views/Round/RoundView.swift | 14 ++- PadelClub/Views/Team/TeamPickerView.swift | 2 +- .../Tournament/TournamentRunningView.swift | 22 ++-- .../Views/Tournament/TournamentView.swift | 2 +- 15 files changed, 195 insertions(+), 160 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 067c40c..53125b9 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -1731,6 +1731,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; ENABLE_PREVIEWS = YES; @@ -1762,6 +1763,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; ENABLE_PREVIEWS = YES; diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 833b61b..4650a0e 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -166,15 +166,14 @@ class Match: ModelObject, Storable { return roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) } - private func _toggleLoserMatchDisableState(_ state: Bool) { + func _toggleLoserMatchDisableState(_ state: Bool) { guard let loserMatch = _loserMatch() else { return } - loserMatch.byeState = state - loserMatch._toggleMatchDisableState(state, forward: true) - - guard let otherMatch = _otherMatch() else { return } - if otherMatch.disabled == state { - loserMatch.byeState = !state + guard let next = _otherMatch() else { return } + loserMatch.byeState = true + if next.disabled { + loserMatch.byeState = false } + loserMatch._toggleMatchDisableState(state, forward: true) } fileprivate func _otherMatch() -> Match? { @@ -202,9 +201,7 @@ class Match: ModelObject, Storable { if next.disabled && byeState == false && next.byeState == false { forwardMatch.byeState = false forwardMatch._toggleMatchDisableState(state, forward: true) - } - - if byeState && next.byeState { + } else if byeState && next.byeState { print("don't disable forward match") forwardMatch.byeState = false forwardMatch._toggleMatchDisableState(false, forward: true) @@ -233,10 +230,10 @@ class Match: ModelObject, Storable { } func _toggleMatchDisableState(_ state: Bool, forward: Bool = false) { - if disabled == state { return } + //if disabled == state { return } disabled = state //byeState = false - try? DataStore.shared.matches.addOrUpdate(instance: self) + //try? DataStore.shared.matches.addOrUpdate(instance: self) _toggleLoserMatchDisableState(state) if forward { diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 66ad6bc..bd96ed2 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -92,26 +92,29 @@ class Round: ModelObject, Storable { func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { return Store.main.filter(isIncluded: { - $0.tournament == tournament && $0.bracketPosition != nil - }).first(where: { - ($0.bracketPosition! / 2) == matchIndex + $0.tournament == tournament + && $0.bracketPosition != nil + && ($0.bracketPosition! / 2) == matchIndex && ($0.bracketPosition! % 2) == team.rawValue - }) + }).first } func seeds(inMatchIndex matchIndex: Int) -> [TeamRegistration] { return Store.main.filter(isIncluded: { - $0.tournament == tournament && $0.bracketPosition != nil - }).filter({ - ($0.bracketPosition! / 2) == matchIndex + $0.tournament == tournament + && $0.bracketPosition != nil + && ($0.bracketPosition! / 2) == matchIndex }) } func seeds() -> [TeamRegistration] { + let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index) + let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index) return Store.main.filter(isIncluded: { - $0.tournament == tournament && $0.bracketPosition != nil - }).filter({ - ($0.bracketPosition! / 2) >= RoundRule.matchIndex(fromRoundIndex: index) && ($0.bracketPosition! / 2) < RoundRule.matchIndex(fromRoundIndex: index) + RoundRule.numberOfMatches(forRoundIndex: index) + $0.tournament == tournament + && $0.bracketPosition != nil + && ($0.bracketPosition! / 2) >= initialMatchIndex + && ($0.bracketPosition! / 2) < initialMatchIndex + numberOfMatches }) } @@ -246,8 +249,8 @@ class Round: ModelObject, Storable { } func getActiveLoserRound() -> Round? { - let rounds = loserRounds() - return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false && $0.isDisabled() == false }).sorted(by: \.index).reversed().first ?? rounds.first(where: { $0.isDisabled() == false }) + let rounds = loserRounds().filter({ $0.isDisabled() == false }).sorted(by: \.index).reversed() + return rounds.first(where: { $0.hasStarted() && $0.hasEnded() == false }) ?? rounds.first } func enableRound() { @@ -299,31 +302,43 @@ class Round: ModelObject, Storable { func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String { - if let parentRound, let initialRound = parentRound.initialRound() { - let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count -// print("initialRound", initialRound.roundTitle()) - if let initialRoundNextRound = initialRound.nextRound() { - let total = initialRoundNextRound.playedMatches().count * 2 + initialRoundNextRound.disabledMatches().count - let previousRoundMatchCount = (previousRound() ?? parentRound).playedMatches().count - let seedInterval = SeedInterval(first: parentMatchCount + total + 1, last: parentMatchCount + total + previousRoundMatchCount).localizedLabel(displayStyle) -// print("seedInterval", seedInterval, parentMatchCount, total, previousRoundMatchCount) - return seedInterval - } + let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index) + let seedsAfterThisRound : [TeamRegistration] = Store.main.filter(isIncluded: { + $0.tournament == tournament + && $0.bracketPosition != nil + && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex + }) + let playedMatches = playedMatches() + let seedInterval = SeedInterval(first: playedMatches.count + seedsAfterThisRound.count + 1, last: playedMatches.count * 2 + seedsAfterThisRound.count) + return seedInterval.localizedLabel(displayStyle) + } + + func seedInterval() -> SeedInterval? { + if loser == nil { + let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index + 1) + let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index) + let seedsAfterThisRound : [TeamRegistration] = Store.main.filter(isIncluded: { + $0.tournament == tournament + && $0.bracketPosition != nil + && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex + }) + let playedMatches = playedMatches() + let reduce = numberOfMatches / 2 - (playedMatches.count + seedsAfterThisRound.count) + return SeedInterval(first: 1, last: numberOfMatches, reduce: reduce) } - return RoundRule.roundName(fromRoundIndex: index) + + if let previousRound = previousRound() { + return previousRound.seedInterval()?.chunks()?.first + } else if let parentRound = parentRound { + return parentRound.seedInterval()?.chunks()?.last + } + + return nil } - + func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String { - if let parentRound, let initialRound = parentRound.initialRound() { - let parentMatchCount = parentRound.theoryCumulativeMatchCount - RoundRule.numberOfMatches(forRoundIndex: initialRound.index) -// print("initialRound", initialRound.roundTitle()) - if let initialRoundNextRound = initialRound.nextRound() { - let total = initialRoundNextRound.playedMatches().count * 2 + initialRoundNextRound.disabledMatches().count - let previousRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: (previousRound() ?? parentRound).index) - let seedInterval = SeedInterval(first: parentMatchCount + total + 1, last: parentMatchCount + total + previousRoundMatchCount).localizedLabel(displayStyle) -// print("seedInterval", seedInterval, parentMatchCount, total, previousRoundMatchCount) - return seedInterval - } + if loser != nil { + return seedInterval()?.localizedLabel(displayStyle) ?? "Pas trouvé" } return RoundRule.roundName(fromRoundIndex: index) } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 129cdd6..074bab5 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -342,9 +342,13 @@ class Tournament : ModelObject, Storable { } } + func allRoundMatches() -> [Match] { + allRounds().flatMap { $0._matches() } + } + func allMatches() -> [Match] { let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id } - let matches: [Match] = unsortedGroupStages.flatMap { $0._matches() } + allRounds().flatMap { $0._matches() } + let matches: [Match] = unsortedGroupStages.flatMap { $0._matches() } + allRoundMatches() return matches.filter({ $0.disabled == false }) } @@ -742,24 +746,27 @@ class Tournament : ModelObject, Storable { let selectedPlayers = selectedPlayers() let paid = selectedPlayers.filter({ $0.hasPaid() }) let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés" - let completion = (Double(paid.count) / Double(selectedPlayers.count)).formatted(.percent.precision(.fractionLength(0))) - return TournamentStatus(label: label, completion: completion) + let completion = (Double(paid.count) / Double(selectedPlayers.count)) + let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) + return TournamentStatus(label: label, completion: completionLabel) } func scheduleStatus() -> TournamentStatus { let allMatches = allMatches() let ready = allMatches.filter({ $0.startDate != nil }) let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés" - let completion = (Double(ready.count) / Double(allMatches.count)).formatted(.percent.precision(.fractionLength(0))) - return TournamentStatus(label: label, completion: completion) + let completion = (Double(ready.count) / Double(allMatches.count)) + let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) + return TournamentStatus(label: label, completion: completionLabel) } func callStatus() -> TournamentStatus { let selectedSortedTeams = selectedSortedTeams() let called = selectedSortedTeams.filter{ $0.called() } let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " paires convoquées" - let completion = (Double(called.count) / Double(selectedSortedTeams.count)).formatted(.percent.precision(.fractionLength(0))) - return TournamentStatus(label: label, completion: completion) + let completion = (Double(called.count) / Double(selectedSortedTeams.count)) + let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) + return TournamentStatus(label: label, completion: completionLabel) } func bracketStatus() -> String { diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index ed94063..3ca6b66 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -797,10 +797,12 @@ enum TournamentType: Int, Hashable, Codable, CaseIterable, Identifiable { } } -enum TeamPosition: Int, Hashable, Codable, CaseIterable { +enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable { case one case two + var id: Int { self.rawValue } + var otherTeam: TeamPosition { switch self { case .one: diff --git a/PadelClub/ViewModel/SeedInterval.swift b/PadelClub/ViewModel/SeedInterval.swift index ff140df..d857a99 100644 --- a/PadelClub/ViewModel/SeedInterval.swift +++ b/PadelClub/ViewModel/SeedInterval.swift @@ -10,24 +10,25 @@ import Foundation struct SeedInterval: Hashable, Comparable { let first: Int let last: Int + var reduce: Int = 0 static func <(lhs: SeedInterval, rhs: SeedInterval) -> Bool { return lhs.first < rhs.first } var count: Int { - dimension + 1 + dimension } private var dimension: Int { - (last - first) + (last - (first - 1)) } func chunks() -> [SeedInterval]? { if dimension > 3 { let split = dimension / 2 - let firstHalf = SeedInterval(first: first, last: first + split - 1) - let secondHalf = SeedInterval(first: first + split, last: last) + let firstHalf = SeedInterval(first: first, last: first + split - 1, reduce: reduce) + let secondHalf = SeedInterval(first: first + split, last: last, reduce: reduce) return [firstHalf, secondHalf] } else { return nil @@ -36,9 +37,9 @@ struct SeedInterval: Hashable, Comparable { func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { if dimension < 2 { - return "#\(first) / #\(last)" + return "#\(first - reduce) / #\(last - reduce)" } else { - return "#\(first) à #\(last)" + return "#\(first - reduce) à #\(last - reduce)" } } } diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 4531718..9bbdbad 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -44,27 +44,27 @@ struct GenericDestinationPickerView: View { .opacity(selectedDestination?.id == destination.id ? 1.0 : 0.4) } .buttonStyle(.plain) - .overlay(alignment: .bottomTrailing) { - if let badge = destination.badgeImage() { - Image(systemName: badge.systemName()) - .foregroundColor(badge.color()) - .imageScale(.medium) - .background ( - Color(.systemBackground) - .clipShape(.circle) - ) - .offset(x: 3, y: 3) - } else if let count = destination.badgeValue(), count > 0 { - Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") - .foregroundColor(.red) - .imageScale(.medium) - .background ( - Color(.systemBackground) - .clipShape(.circle) - ) - .offset(x: 3, y: 3) - } - } +// .overlay(alignment: .bottomTrailing) { +// if let badge = destination.badgeImage() { +// Image(systemName: badge.systemName()) +// .foregroundColor(badge.color()) +// .imageScale(.medium) +// .background ( +// Color(.systemBackground) +// .clipShape(.circle) +// ) +// .offset(x: 3, y: 3) +// } else if let count = destination.badgeValue(), count > 0 { +// Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") +// .foregroundColor(.red) +// .imageScale(.medium) +// .background ( +// Color(.systemBackground) +// .clipShape(.circle) +// ) +// .offset(x: 3, y: 3) +// } +// } } } .fixedSize() diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index be37b09..6d5dda4 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -48,8 +48,11 @@ struct MatchRowView: View { // Button("toggle fwrd match") { // match._toggleForwardMatchDisableState(true) // } +// Button("toggle loser match") { +// match._toggleLoserMatchDisableState(true) +// } // }) -// + NavigationLink { MatchDetailView(match: match, matchViewStyle: matchViewStyle) } label: { diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 6a26fd6..c29289a 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -14,8 +14,17 @@ struct MatchSetupView: View { @ViewBuilder var body: some View { - _teamView(inTeamPosition: .one) - _teamView(inTeamPosition: .two) + ForEach(TeamPosition.allCases) { teamPosition in + VStack(alignment: .leading) { + if teamPosition == .one { + Text("Branche du haut") + } + _teamView(inTeamPosition: teamPosition) + if teamPosition == .two { + Text("Branche du bas") + } + } + } } @ViewBuilder @@ -33,6 +42,7 @@ struct MatchSetupView: View { if match.isSeededBy(team: team, inTeamPosition: teamPosition) { team.bracketPosition = nil match.enableMatch() + try? dataStore.matches.addOrUpdate(instance: match) try? dataStore.teamRegistrations.addOrUpdate(instance: team) } else { match.teamWillBeWalkOut(team) @@ -88,7 +98,7 @@ struct MatchSetupView: View { } } } label: { - Text("Tirage").tag(nil as SeedInterval?) + Text("Tirer au sort").tag(nil as SeedInterval?) } .disabled(availableSeedGroups.isEmpty && walkOutSpot == false) @@ -106,9 +116,8 @@ struct MatchSetupView: View { } } .fixedSize(horizontal: false, vertical: true) - .buttonBorderShape(.capsule) - .buttonStyle(.borderedProminent) - + .buttonStyle(.borderless) + .underline() } } } diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift index a744f07..424b7f4 100644 --- a/PadelClub/Views/Round/LoserRoundView.swift +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -29,7 +29,7 @@ struct LoserRoundView: View { ForEach(matches) { match in MatchRowView(match: match, matchViewStyle: .standardStyle) .overlay { - if match.disabled && isEditingTournamentSeed { + if match.disabled /*&& isEditingTournamentSeed*/ { Image(systemName: "xmark") .resizable() .scaledToFit() diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 9020a1e..0921324 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -7,37 +7,44 @@ import SwiftUI -enum LoserRound: Identifiable, Selectable { - case round(turnIndex: Int, rounds: [Round]) +struct LoserRound: Identifiable, Selectable { + let turnIndex: Int + let rounds: [Round] var id: Int { - switch self { - case .round(let turnIndex, _): - return turnIndex + return turnIndex + } + + + static func updateDestinations(fromLoserRounds loserRounds: [Round], inUpperBracketRound upperBracketRound: Round) -> [LoserRound] { + var rounds = [LoserRound]() + for (index, round) in loserRounds.enumerated() { + rounds.append(LoserRound(turnIndex: index, rounds: upperBracketRound.loserRounds(forRoundIndex: round.index))) + } + + return rounds + } + + static func enabledLoserRounds(inLoserRounds loserRounds: [Round], inUpperBracketRound upperBracketRound: Round) -> [Round] { + return loserRounds.filter { loserRound in + upperBracketRound.loserRounds(forRoundIndex: loserRound.index).anySatisfy({ $0.isDisabled() == false }) } } + + } extension LoserRound { func selectionLabel() -> String { - switch self { - case .round(let turnIndex, _): - return "Tour #\(turnIndex + 1)" - } + return "Tour #\(turnIndex + 1)" } func badgeValue() -> Int? { - switch self { - case .round(_, let rounds): - return rounds.flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count - } + return rounds.flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count } func badgeImage() -> Badge? { - switch self { - case .round(_, let rounds): - return rounds.allSatisfy({ $0.hasEnded() }) ? .checkmark : nil - } + return rounds.allSatisfy({ $0.hasEnded() }) ? .checkmark : nil } } @@ -47,54 +54,34 @@ struct LoserRoundsView: View { var upperBracketRound: Round @State private var selectedRound: LoserRound? let loserRounds: [Round] - + @State private var allDestinations: [LoserRound] + init(upperBracketRound: Round) { self.upperBracketRound = upperBracketRound - self.loserRounds = upperBracketRound.loserRounds() -// _selectedRound = State(wrappedValue: upperBracketRound.getActiveLoserRound()) + let _loserRounds = upperBracketRound.loserRounds() + self.loserRounds = _loserRounds + let enabledLoserRounds = LoserRound.enabledLoserRounds(inLoserRounds: _loserRounds, inUpperBracketRound: upperBracketRound) + let rounds = LoserRound.updateDestinations(fromLoserRounds: enabledLoserRounds, inUpperBracketRound: upperBracketRound) + _allDestinations = State(wrappedValue: rounds) + + _selectedRound = State(wrappedValue: rounds.first(where: { $0.rounds.anySatisfy({ $0.getActiveLoserRound() != nil }) }) ?? rounds.first) } - func allDestinations() -> [LoserRound] { -// return loserRounds - if isEditingTournamentSeed.wrappedValue == false { - let loserRounds = loserRounds.filter { loserRound in - upperBracketRound.loserRounds(forRoundIndex: loserRound.index).anySatisfy({ $0.isDisabled() == false }) - } - - var rounds = [LoserRound]() - for (index, round) in loserRounds.enumerated() { - rounds.append(LoserRound.round(turnIndex: index, rounds: upperBracketRound.loserRounds(forRoundIndex: round.index))) - } - - return rounds - } else { - var rounds = [LoserRound]() - for (index, round) in loserRounds.enumerated() { - rounds.append(LoserRound.round(turnIndex: index, rounds: upperBracketRound.loserRounds(forRoundIndex: round.index))) - } - - return rounds - } - } - var body: some View { VStack(spacing: 0) { - GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: allDestinations(), nilDestinationIsValid: true) - switch selectedRound { - case .none: - List { - RowButtonView("Effacer", role: .destructive) { - - } - } - case .some(let selectedRound): - switch selectedRound { - case .round(_, let rounds): - LoserRoundView(loserRounds: rounds) - } - } + GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: allDestinations, nilDestinationIsValid: false) + LoserRoundView(loserRounds: selectedRound!.rounds) } .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) + .onChange(of: isEditingTournamentSeed.wrappedValue) { + _updateDestinations() + } + } + + private func _updateDestinations() { + let enabledLoserRounds = isEditingTournamentSeed.wrappedValue ? loserRounds : LoserRound.enabledLoserRounds(inLoserRounds: loserRounds, inUpperBracketRound: upperBracketRound) + + self.allDestinations = LoserRound.updateDestinations(fromLoserRounds: enabledLoserRounds, inUpperBracketRound: upperBracketRound) } } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index e78e1f2..7d78d83 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -21,7 +21,7 @@ struct RoundView: View { let loserRounds = round.loserRounds() //(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue }) if loserRounds.isEmpty == false, let first = loserRounds.first { - let correspondingLoserRoundTitle = first.correspondingLoserRoundTitle() + let correspondingLoserRoundTitle = round.correspondingLoserRoundTitle() Section { NavigationLink { LoserRoundsView(upperBracketRound: round) @@ -36,9 +36,8 @@ struct RoundView: View { RowButtonView("Placer \(availableSeedGroup.localizedLabel())") { tournament.setSeeds(inRoundIndex: round.index, inSeedGroup: availableSeedGroup) - try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds()) - if tournament.availableSeeds().isEmpty { + _save() self.isEditingTournamentSeed.wrappedValue = false } } @@ -56,11 +55,20 @@ struct RoundView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { + if isEditingTournamentSeed.wrappedValue { + _save() + } isEditingTournamentSeed.wrappedValue.toggle() } } } } + + private func _save() { + try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds()) + let allRoundMatches = tournament.allRoundMatches() + try? DataStore.shared.matches.addOrUpdate(contentOfs: allRoundMatches) + } } #Preview { diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift index 7747d5f..012f95d 100644 --- a/PadelClub/Views/Team/TeamPickerView.swift +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -23,7 +23,7 @@ struct TeamPickerView: View { .sheet(isPresented: $presentTeamPickerView) { NavigationStack { List { - let teams = tournament.sortedTeams() + let teams = tournament.selectedSortedTeams() if luckyLosers.isEmpty == false { Section { _teamListView(luckyLosers.sorted(by: \.weight)) diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index da4909c..9250f04 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -8,8 +8,14 @@ import SwiftUI struct TournamentRunningView: View { - @Environment(Tournament.self) private var tournament: Tournament - + var tournament: Tournament + var allMatches: [Match] + + init(tournament: Tournament) { + self.tournament = tournament + self.allMatches = tournament.allMatches() + } + @ViewBuilder var body: some View { Section { @@ -68,15 +74,13 @@ struct TournamentRunningView: View { } } - let allMatches = tournament.allMatches() - MatchListView(section: "en cours", matches: tournament.runningMatches(allMatches)).id(UUID()) - MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches)).id(UUID()) - MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches)).id(UUID()) - MatchListView(section: "terminés", matches: tournament.finishedMatches(allMatches)).id(UUID()) + MatchListView(section: "en cours", matches: tournament.runningMatches(allMatches)) + //MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches)) + //MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches)) + MatchListView(section: "terminés", matches: tournament.finishedMatches(allMatches)) } } #Preview { - TournamentRunningView() - .environment(Tournament.mock()) + TournamentRunningView(tournament: Tournament.mock()) } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index f13aafb..731ee8f 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -66,7 +66,7 @@ struct TournamentView: View { case .initial: TournamentInitView() case .build: - TournamentRunningView() + TournamentRunningView(tournament: tournament) } } .toolbarBackground(.visible, for: .navigationBar)