From 2f14a1685252d72d75f573c83fac9c5f43358de6 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 17 Sep 2024 09:47:16 +0200 Subject: [PATCH 1/5] wip --- PadelClub.xcodeproj/project.pbxproj | 18 +-- .../Coredata/ImportedPlayer+Extensions.swift | 4 + .../Data/Federal/FederalTournament.swift | 13 ++ PadelClub/Data/Round.swift | 25 +++- PadelClub/Data/Tournament.swift | 2 + PadelClub/Utils/ContactManager.swift | 3 + PadelClub/Views/Match/MatchSetupView.swift | 3 +- .../Agenda/TournamentSubscriptionView.swift | 130 +++++++++++++++++- .../Components/DateUpdateManagerView.swift | 105 ++++++++++++++ .../LoserRoundScheduleEditorView.swift | 2 +- .../LoserRoundStepScheduleEditorView.swift | 13 +- .../Planning/MatchScheduleEditorView.swift | 4 + PadelClub/Views/Team/EditingTeamView.swift | 2 +- PadelClub/Views/Team/TeamPickerView.swift | 10 ++ .../Views/Tournament/Screen/AddTeamView.swift | 127 ++++++++++++----- .../TournamentGeneralSettingsView.swift | 18 ++- .../Screen/InscriptionManagerView.swift | 6 +- 17 files changed, 421 insertions(+), 64 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f669a1a..2624177 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -2546,7 +2546,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -2556,6 +2556,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -2568,7 +2569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.8; + MARKETING_VERSION = 1.0.9; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2588,7 +2589,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -2597,6 +2598,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -2609,7 +2611,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.8; + MARKETING_VERSION = 1.0.9; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2701,7 +2703,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -2723,7 +2725,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.8; + MARKETING_VERSION = 1.0.9; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2743,7 +2745,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -2764,7 +2766,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.8; + MARKETING_VERSION = 1.0.9; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift index 1745db9..ab7d66f 100644 --- a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift +++ b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift @@ -70,6 +70,10 @@ extension ImportedPlayer: PlayerHolder { } } + func contains(_ searchField: String) -> Bool { + firstName?.localizedCaseInsensitiveContains(searchField) == true || lastName?.localizedCaseInsensitiveContains(searchField) == true + } + func hitForSearch(_ searchText: String) -> Int { var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current) trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ") diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index a535417..16360ce 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -151,6 +151,19 @@ struct FederalTournament: Identifiable, Codable { [libelle, dateDebut?.formatted(date: .complete, time: .omitted)].compactMap({$0}).joined(separator: "\n") + "\n" } + var sharePartnerMessage: String { + ["Je nous ai inscris au tournoi suivant : ", + libelle, + dateDebut?.formatted(date: .complete, time: .omitted), + "message preparé par Padel Club", + URLs.appStore.rawValue + ].compactMap({$0}).joined(separator: "\n") + "\n" + } + + func calendarNoteMessage() -> String { + [jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, installation?.telephone].compactMap({$0}).joined(separator: "\n") + } + var japMessage: String { [nomClub, jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, installation?.telephone].compactMap({$0}).joined(separator: ";") } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 0cc15be..6fd9425 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -153,7 +153,12 @@ final class Round: ModelObject, Storable { let teamIds: [String] = self._matches().compactMap { $0.losingTeamId } return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) } } - + + func winners() -> [TeamRegistration] { + let teamIds: [String] = self._matches().compactMap { $0.winningTeamId } + return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) } + } + func teams() -> [TeamRegistration] { return playedMatches().flatMap({ $0.teams() }) } @@ -186,13 +191,13 @@ defer { case .two: if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) { return luckyLoser.team - } else if groupStageLoserBracket == false, let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) { + } else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) { if let teamId = previousMatch.winningTeamId { return self.tournamentStore.teamRegistrations.findById(teamId) } else if previousMatch.disabled { return previousMatch.teams().first } - } else if groupStageLoserBracket == false, let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId { + } else if let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId { return tournamentStore.findById(parent) } } @@ -208,8 +213,13 @@ defer { print("func upperBracketTopMatch", matchIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif + let parentRound = parentRound + if parentRound?.parent == nil, groupStageLoserBracket == false, tournamentObject()?.automaticLoserBracket == false { + return nil + } + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) - if isLoserBracket(), previousRound == nil, let parentRound = parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) { + if isLoserBracket(), previousRound == nil, let upperBracketTopMatch = parentRound?.getMatch(atMatchIndexInRound: indexInRound * 2) { return upperBracketTopMatch } return nil @@ -224,8 +234,13 @@ defer { } #endif + let parentRound = parentRound + if parentRound?.parent == nil, groupStageLoserBracket == false, tournamentObject()?.automaticLoserBracket == false { + return nil + } + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) - if isLoserBracket(), previousRound == nil, let parentRound = parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) { + if isLoserBracket(), previousRound == nil, let upperBracketBottomMatch = parentRound?.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) { return upperBracketBottomMatch } return nil diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index e042f11..3f478da 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -57,6 +57,7 @@ final class Tournament : ModelObject, Storable { var publishTournament: Bool = false var hidePointsEarned: Bool = false var publishRankings: Bool = false + var automaticLoserBracket: Bool = true @ObservationIgnored var navigationPath: [Screen] = [] @@ -2189,3 +2190,4 @@ extension Tournament { } } + diff --git a/PadelClub/Utils/ContactManager.swift b/PadelClub/Utils/ContactManager.swift index 07b23a2..88898bf 100644 --- a/PadelClub/Utils/ContactManager.swift +++ b/PadelClub/Utils/ContactManager.swift @@ -15,6 +15,9 @@ enum ContactManagerError: LocalizedError { case mailNotSent //no network no error case messageFailed case messageNotSent //no network no error + case calendarAccessDenied + case calendarEventSaveFailed + case noCalendarAvailable } enum ContactType: Identifiable { diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 1998374..094dc80 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -63,7 +63,7 @@ struct MatchSetupView: View { } HStack { let luckyLosers = walkOutSpot ? match.luckyLosers() : [] - TeamPickerView(shouldConfirm: shouldConfirm, groupStagePosition: nil, matchTypeContext: matchTypeContext, luckyLosers: luckyLosers, teamPicked: { team in + TeamPickerView(shouldConfirm: shouldConfirm, round: match.roundObject, matchTypeContext: matchTypeContext, luckyLosers: luckyLosers, teamPicked: { team in print(team.pasteData()) if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket { match.setLuckyLoser(team: team, teamPosition: teamPosition) @@ -86,6 +86,7 @@ struct MatchSetupView: View { } } }) + .environment(match) if matchTypeContext == .bracket, let tournament = match.currentTournament() { let availableQualifiedTeams = tournament.availableQualifiedTeams() let availableSeedGroups = tournament.availableSeedGroups() diff --git a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift index 41e586e..bc75ddc 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import EventKit struct TournamentSubscriptionView: View { @EnvironmentObject var networkMonitor: NetworkMonitor @@ -17,6 +18,8 @@ struct TournamentSubscriptionView: View { @State private var selectedPlayers: [ImportedPlayer] @State private var contactType: ContactType? = nil @State private var sentError: ContactManagerError? = nil + @State private var didSendMessage: Bool = false + @State private var didSaveInCalendar: Bool = false init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: User) { self.federalTournament = federalTournament @@ -25,8 +28,79 @@ struct TournamentSubscriptionView: View { _selectedPlayers = .init(wrappedValue: [user.currentPlayerData()].compactMap({ $0 })) } + func hasPartner() -> Bool { + selectedPlayers.count == 2 + } + + func inscriptionSent() -> Bool { + didSendMessage + } + + func addEvent() { + let eventStore = EKEventStore() + + eventStore.requestWriteOnlyAccessToEvents { (granted, error) in + if granted && error == nil { + print("Access granted") + let startDate = federalTournament.startDate + let endDate = federalTournament.dateFin ?? federalTournament.startDate.endOfDay() + addEventToCalendar(title: messageSubject, startDate: startDate, endDate: endDate) + didSaveInCalendar = true + } else { + print("Access denied or error occurred: \(String(describing: error?.localizedDescription))") + sentError = .calendarAccessDenied + } + } + + } + + func addEventToCalendar(title: String, startDate: Date, endDate: Date) { + let eventStore = EKEventStore() + if eventStore.defaultCalendarForNewEvents == nil { + sentError = .noCalendarAvailable + return + } + eventStore.requestWriteOnlyAccessToEvents { (granted, error) in + if granted && error == nil { + let event = EKEvent(eventStore: eventStore) + event.title = title + event.isAllDay = true + event.startDate = startDate + event.endDate = endDate + event.calendar = eventStore.defaultCalendarForNewEvents + event.notes = noteCalendar + event.location = federalTournament.clubLabel() + do { + try eventStore.save(event, span: .thisEvent) + didSaveInCalendar = true + print("Event saved") + } catch let error { + print("Failed to save event: \(error.localizedDescription)") + sentError = .calendarEventSaveFailed + } + } else { + print("Access denied or error occurred: \(String(describing: error?.localizedDescription))") + sentError = .calendarAccessDenied + } + } + } + + var body: some View { List { + if didSaveInCalendar { + Section { + LabeledContent { + Image(systemName: "checkmark").foregroundStyle(.green) + } label: { + Text("Le tournoi a bien été ajouté dans votre calendrier par défaut") + let eventStore = EKEventStore() + if let defaultCalendarForNewEvents = eventStore.defaultCalendarForNewEvents { + Text(defaultCalendarForNewEvents.title) + } + } + } + } Section { LabeledContent("Tournoi") { Text(federalTournament.libelle ?? "Tournoi") @@ -121,6 +195,17 @@ struct TournamentSubscriptionView: View { } .toolbar(content: { Menu { + ShareLink(item: federalTournament.sharePartnerMessage) { + Label("Prévenir votre partenaire", systemImage: "person.2") + } + + Button("Ajouter à votre agenda") { + addEvent() + } + + ShareLink(item: federalTournament.shareMessage) { + Label("Partager les infos", systemImage: "info") + } Link(destination: URL(string:"https://tenup.fft.fr/tournoi/\(federalTournament.id)")!) { Label("Voir sur Tenup", systemImage: "tennisball") } @@ -134,6 +219,13 @@ struct TournamentSubscriptionView: View { .alert("Un problème est survenu", isPresented: messageSentFailed) { Button("OK") { } + + if sentError == .calendarAccessDenied || sentError == .noCalendarAvailable { + Button("Voir vos réglages") { + openAppSettings() + } + } + } message: { Text(_networkErrorMessage) } @@ -150,6 +242,8 @@ struct TournamentSubscriptionView: View { case .sent: if networkMonitor.connected == false { self.sentError = .messageNotSent + } else { + self.didSendMessage = true } @unknown default: break @@ -167,6 +261,8 @@ struct TournamentSubscriptionView: View { if networkMonitor.connected == false { self.contactType = nil self.sentError = .mailNotSent + } else { + self.didSendMessage = true } @unknown default: break @@ -188,12 +284,19 @@ struct TournamentSubscriptionView: View { var messageBody: String { let bonjourOuBonsoir = Date().timeOfDay.hello let bonneSoireeOuBonneJournee = Date().timeOfDay.goodbye - let body = [["\(bonjourOuBonsoir),\n\nJe souhaiterais inscrire mon équipe au tournoi : ", build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, "\nCordialement,\n", user.fullName() ?? bonneSoireeOuBonneJournee, "----------------------------------\nCe message a été préparé grâce à l'application Padel Club !\nVotre tournoi n'est pas encore dessus ? \(URLs.main.rawValue)", "Téléchargez l'app : \(URLs.appStore.rawValue)", "En savoir plus : \(URLs.appDescription.rawValue)"].compactMap { $0 }.joined(separator: "\n") + "\n" + let body = [["\(bonjourOuBonsoir),\n\nJe souhaiterais inscrire mon équipe au tournoi : ", build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, "\nCordialement,\n", user.fullName() ?? bonneSoireeOuBonneJournee, "----------------------------------\nCe message a été préparé grâce à l'application Padel Club !"].compactMap { $0 }.joined(separator: "\n") + "\n" return body } var messageBodyShort: String { - let body = [[build.buildHolderTitle(), federalTournament.clubLabel()].compacted().joined(separator: " "), federalTournament.computedStartDate, teamsString].compacted().joined(separator: "\n") + "\n" + let bonjourOuBonsoir = Date().timeOfDay.hello + let bonneSoireeOuBonneJournee = Date().timeOfDay.goodbye + let body = [["\(bonjourOuBonsoir),\n\nJe souhaiterais inscrire mon équipe au tournoi : ", build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, "\nCordialement,\n", user.fullName() ?? bonneSoireeOuBonneJournee].compactMap { $0 }.joined(separator: "\n") + "\n" + return body + } + + var noteCalendar: String { + let body = [[build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, federalTournament.calendarNoteMessage()].compactMap { $0 }.joined(separator: "\n") + "\n" return body } @@ -227,6 +330,29 @@ struct TournamentSubscriptionView: View { if sentError == .mailFailed { errors.append("Le mail n'a pas été envoyé") } + + if sentError == .calendarAccessDenied { + errors.append("Padel Club n'a pas accès à votre calendrier") + } + + if sentError == .calendarEventSaveFailed { + errors.append("Padel Club n'a pas réussi à sauver ce tournoi dans votre calendrier") + } + + if sentError == .noCalendarAvailable { + errors.append("Padel Club n'a pas réussi à trouver un calendrier pour y inscrire ce tournoi") + } + + return errors.joined(separator: "\n") } + + func openAppSettings() { + if let appSettings = URL(string: UIApplication.openSettingsURLString) { + if UIApplication.shared.canOpenURL(appSettings) { + UIApplication.shared.open(appSettings, options: [:], completionHandler: nil) + } + } + } + } diff --git a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift index 3151d65..c3198cc 100644 --- a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift +++ b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift @@ -93,6 +93,7 @@ struct DatePickingView: View { } struct MatchFormatPickingView: View { + var title: String? = nil @Binding var matchFormat: MatchFormat var validateAction: (() async -> ()) @@ -110,6 +111,10 @@ struct MatchFormatPickingView: View { confirmScheduleUpdate = false } } + } header: { + if let title { + Text(title) + } } footer: { if confirmScheduleUpdate && updatingInProgress == false { FooterButtonView("non, ne pas modifier les horaires") { @@ -123,3 +128,103 @@ struct MatchFormatPickingView: View { } } } + + +struct DatePickingViewWithFormat: View { + @Binding var matchFormat: MatchFormat + let title: String + @Binding var startDate: Date + @Binding var currentDate: Date? + var duration: Int? + var validateAction: ((Bool) async -> ()) + + @State private var confirmScheduleUpdate: Bool = false + @State private var updatingInProgress : Bool = false + + var body: some View { + Section { + MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat) + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + if confirmScheduleUpdate { + RowButtonView("Sauver et modifier la suite") { + updatingInProgress = true + await validateAction(true) + updatingInProgress = false + confirmScheduleUpdate = false + } + } + } header: { + Text(title) + } footer: { + if confirmScheduleUpdate && updatingInProgress == false { + HStack { + FooterButtonView("sauver sans modifier la suite") { + Task { + await validateAction(false) + confirmScheduleUpdate = false + } + } + Text("ou") + FooterButtonView("annuler") { + confirmScheduleUpdate = false + } + } + } else { + HStack { + Menu { + Button("de 30 minutes") { + startDate = startDate.addingTimeInterval(1800) + } + + Button("d'une heure") { + startDate = startDate.addingTimeInterval(3600) + } + + Button("à 9h") { + startDate = startDate.atNine() + } + + Button("à demain 9h") { + startDate = startDate.tomorrowAtNine + } + + if let duration { + Button("à la prochaine rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * 60) + } + Button("à la précédente rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * -60) + } + } + } label: { + Text("décaler") + .underline() + } + .buttonStyle(.borderless) + Spacer() + + if currentDate != nil { + FooterButtonView("retirer l'horaire bloqué") { + currentDate = nil + } + } else { + FooterButtonView("bloquer l'horaire") { + currentDate = startDate + } + } + } + .buttonStyle(.borderless) + } + + } + .headerProminence(.increased) + .onChange(of: matchFormat) { + confirmScheduleUpdate = true + } + .onChange(of: startDate) { + confirmScheduleUpdate = true + } + } +} diff --git a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift index 33b2175..f280dd2 100644 --- a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift @@ -33,7 +33,7 @@ struct LoserRoundScheduleEditorView: View { var body: some View { List { - MatchFormatPickingView(matchFormat: $matchFormat) { + MatchFormatPickingView(title: "Format des tours par défault", matchFormat: $matchFormat) { await _updateSchedule() } diff --git a/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift index d497e7e..6ee09ec 100644 --- a/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift @@ -38,8 +38,11 @@ struct LoserRoundStepScheduleEditorView: View { var body: some View { @Bindable var round = round - DatePickingView(title: "Tour #\(stepIndex + 1)", startDate: $startDate, currentDate: .constant(nil), duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { - await _updateSchedule() + DatePickingViewWithFormat(matchFormat: $round.matchFormat, title: "Tour #\(stepIndex + 1)", startDate: $startDate, currentDate: .constant(nil), duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { update in + for match in matches { + match.matchFormat = round.matchFormat + } + await _updateSchedule(update: update) } Section { @@ -64,8 +67,10 @@ struct LoserRoundStepScheduleEditorView: View { } } - private func _updateSchedule() async { - tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) + private func _updateSchedule(update: Bool) async { + if update { + tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) + } upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in round.startDate = startDate }) diff --git a/PadelClub/Views/Planning/MatchScheduleEditorView.swift b/PadelClub/Views/Planning/MatchScheduleEditorView.swift index 695adf7..95a1bce 100644 --- a/PadelClub/Views/Planning/MatchScheduleEditorView.swift +++ b/PadelClub/Views/Planning/MatchScheduleEditorView.swift @@ -27,6 +27,10 @@ struct MatchScheduleEditorView: View { } var body: some View { + MatchFormatPickingView(matchFormat: $match.matchFormat) { + await _updateSchedule() + } + DatePickingView(title: title, startDate: $startDate, currentDate: .constant(nil), duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { await _updateSchedule() } diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 5a03ef6..2c017db 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -243,7 +243,7 @@ struct EditingTeamView: View { } .tint(.master) } - .sheet(item: $editedTeam) { editedTeam in + .fullScreenCover(item: $editedTeam) { editedTeam in NavigationStack { AddTeamView(tournament: tournament, editedTeam: editedTeam) } diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift index a9b143c..0515283 100644 --- a/PadelClub/Views/Team/TeamPickerView.swift +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -18,6 +18,7 @@ struct TeamPickerView: View { var shouldConfirm: Bool = false var groupStagePosition: Int? = nil + var round: Round? = nil var matchTypeContext: MatchType = .bracket var luckyLosers: [TeamRegistration] = [] let teamPicked: ((TeamRegistration) -> (Void)) @@ -39,6 +40,14 @@ struct TeamPickerView: View { .sheet(isPresented: $presentTeamPickerView) { NavigationStack { List { + if matchTypeContext == .loserBracket, let losers = round?.parentRound?.losers() { + _sectionView(losers.sorted(by: \.weight, order: sortOrder), title: "Perdant du tour précédent") + } + + if matchTypeContext == .loserBracket, let losers = round?.previousRound()?.winners() { + _sectionView(losers.sorted(by: \.weight, order: sortOrder), title: "Gagnant du tour précédent") + } + if let groupStagePosition, let replacementRangeExtended = tournament.replacementRangeExtended(groupStagePosition: groupStagePosition) { Section { GroupStageTeamReplacementView.TeamRangeView(teamRange: replacementRangeExtended, playerWeight: 0) @@ -130,6 +139,7 @@ struct TeamPickerView: View { } .frame(maxWidth: .infinity) .buttonStyle(.plain) + .listRowView(isActive: matchTypeContext == .loserBracket && round?.teams().map({ $0.id }).contains(team.id) == true, color: .green, hideColorVariation: true) // .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) { // Button("Retirer du tableau", role: .destructive) { // teamPicked(confirmTeam!) diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index f97e3a7..9b200ae 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -20,7 +20,11 @@ struct AddTeamView: View { var tournament: Tournament var cancelShouldDismiss: Bool = false + enum FocusField: Hashable { + case pasteField + } + @FocusState private var focusedField: FocusField? @State private var searchField: String = "" @State private var presentSearch: Bool = false @State private var presentPlayerSearch: Bool = false @@ -36,7 +40,8 @@ struct AddTeamView: View { @State private var confirmDuplicate: Bool = false @State private var homonyms: [PlayerRegistration] = [] @State private var confirmHomonym: Bool = false - + @State private var editableTextField: String = "" + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -61,13 +66,32 @@ struct AddTeamView: View { _pasteString = .init(wrappedValue: pasteString) _fetchPlayers = FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], predicate: SearchViewModel.pastePredicate(pasteField: pasteString, mostRecentDate: tournament.rankSourceDate, filterOption: tournament.tournamentCategory.playerFilterOption)) _autoSelect = .init(wrappedValue: true) + _editableTextField = .init(wrappedValue: pasteString) cancelShouldDismiss = true } } var body: some View { - _buildingTeamView() - .navigationBarBackButtonHidden(true) + if pasteString == nil { + computedBody + } else { + computedBody + .searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .automatic), prompt: Text("Chercher dans les résultats")) + } + } + + var computedBody: some View { + List(selection: $createdPlayerIds) { + _buildingTeamView() + } + .onReceive(fetchPlayers.publisher.count()) { _ in // <-- here + if let pasteString, count == 2, autoSelect == true { + fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in + createdPlayerIds.insert(player.license!) + } + autoSelect = false + } + } .alert("Présence d'homonyme", isPresented: $confirmHomonym) { Button("Créer l'équipe quand même") { _createTeam(checkDuplicates: false, checkHomonym: false) @@ -121,7 +145,6 @@ struct AddTeamView: View { } .tint(.master) } - .navigationTitle(editedTeam == nil ? "Ajouter une équipe" : "Modifier l'équipe") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Annuler", role: .cancel) { @@ -130,27 +153,23 @@ struct AddTeamView: View { } if pasteString == nil { - ToolbarItem(placement: .topBarTrailing) { + ToolbarItem(placement: .bottomBar) { PasteButton(payloadType: String.self) { strings in guard let first = strings.first else { return } - Task { - await MainActor.run { - fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) - fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] - pasteString = first - autoSelect = true - } - } + handlePasteString(first) } .foregroundStyle(.master) - .labelStyle(.iconOnly) + .labelStyle(.titleAndIcon) .buttonBorderShape(.capsule) } } } .navigationBarBackButtonHidden(_isEditingTeam()) .toolbarBackground(.visible, for: .navigationBar) + .toolbarBackground(.visible, for: .bottomBar) .navigationBarTitleDisplayMode(.inline) + .navigationTitle(editedTeam == nil ? "Ajouter une équipe" : "Modifier l'équipe") + .environment(\.editMode, Binding.constant(EditMode.active)) } private func _isEditingTeam() -> Bool { @@ -275,6 +294,8 @@ struct AddTeamView: View { createdPlayers.removeAll() createdPlayerIds.removeAll() pasteString = nil + editableTextField = "" + if team.players().count > 1 { dismiss() } @@ -302,23 +323,42 @@ struct AddTeamView: View { createdPlayers.removeAll() createdPlayerIds.removeAll() pasteString = nil + editableTextField = "" + self.editedTeam = nil if editedTeam.players().count > 1 { dismiss() } } + @ViewBuilder private func _buildingTeamView() -> some View { - List(selection: $createdPlayerIds) { if let pasteString { - Section { - Text(pasteString) + TextEditor(text: $editableTextField) + .focused($focusedField, equals: .pasteField) + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Button("Fermer", role: .cancel) { + self.editableTextField = pasteString + self.focusedField = nil + } + Spacer() + Button("Chercher") { + self.handlePasteString(editableTextField) + self.focusedField = nil + } + .buttonStyle(.bordered) + } + } + } header: { + Text("Contenu du presse-papier") } footer: { HStack { - Text("contenu du presse-papier") Spacer() Button("effacer", role: .destructive) { + self.focusedField = nil + self.editableTextField = "" self.pasteString = nil self.createdPlayers.removeAll() self.createdPlayerIds.removeAll() @@ -387,6 +427,8 @@ struct AddTeamView: View { Text(tournament.cutLabel(index: teamIndex, teamCount: selectedSortedTeams.count)) } } +// } else { +// Text("Préparation de l'équipe") } } @@ -404,33 +446,16 @@ struct AddTeamView: View { RowButtonView("Effacer cette recherche") { self.pasteString = nil + self.editableTextField = "" } } } else { - Section { - let sortedPlayers = fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }) - ForEach(sortedPlayers) { player in - ImportedPlayerView(player: player).tag(player.license!) - } - } header: { - Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix) - } + _listOfPlayers(pasteString: pasteString) } } else { _managementView() } - } - .headerProminence(.increased) - .onReceive(fetchPlayers.publisher.count()) { _ in // <-- here - if let pasteString, count == 2, autoSelect == true { - fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in - createdPlayerIds.insert(player.license!) - } - autoSelect = false - } - } - .environment(\.editMode, Binding.constant(EditMode.active)) } private var count: Int { @@ -453,4 +478,32 @@ struct AddTeamView: View { Logger.error(error) } } + + private func handlePasteString(_ first: String) { + Task { + await MainActor.run { + fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) + fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] + pasteString = first + editableTextField = first + autoSelect = true + } + } + + } + + + @ViewBuilder + private func _listOfPlayers(pasteString: String) -> some View { + let sortedPlayers = fetchPlayers.filter({ $0.contains(searchField) || searchField.isEmpty }).sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }) + + Section { + ForEach(sortedPlayers) { player in + ImportedPlayerView(player: player).tag(player.license!) + } + } header: { + Text(sortedPlayers.count.formatted() + " résultat" + sortedPlayers.count.pluralSuffix) + } + } + } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index ed37d53..11416ea 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -11,7 +11,7 @@ import LeStorage struct TournamentGeneralSettingsView: View { @EnvironmentObject var dataStore: DataStore - var tournament: Tournament + @Bindable var tournament: Tournament @State private var tournamentName: String = "" @State private var entryFee: Double? = nil @FocusState private var focusedField: Tournament.CodingKeys? @@ -24,7 +24,7 @@ struct TournamentGeneralSettingsView: View { var body: some View { @Bindable var tournament = tournament - Form { + Form { Section { TournamentDatePickerView() TournamentDurationManagerView() @@ -34,6 +34,20 @@ struct TournamentGeneralSettingsView: View { TournamentLevelPickerView() } + Section { + Toggle(isOn: $tournament.automaticLoserBracket) { + Text("Gestion automatique des matchs de classements") + } +// Picker(selection: $tournament.loserBracketMode) { +// ForEach(LoserBracketMode.allCases) { mode in +// Text(mode.loserBracketModeLocalizedLabel()).tag(mode) +// } +// } label: { +// Text("Mode") +// } + } + + Section { LabeledContent { TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 6c80e60..e656aea 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -327,7 +327,7 @@ struct InscriptionManagerView: View { UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament) .tint(.master) } - .sheet(isPresented: $presentAddTeamView, onDismiss: { + .fullScreenCover(isPresented: $presentAddTeamView, onDismiss: { _setHash() }) { NavigationStack { @@ -335,7 +335,7 @@ struct InscriptionManagerView: View { } .tint(.master) } - .sheet(item: $editedTeam, onDismiss: { + .fullScreenCover(item: $editedTeam, onDismiss: { _setHash() }) { editedTeam in NavigationStack { @@ -343,7 +343,7 @@ struct InscriptionManagerView: View { } .tint(.master) } - .sheet(item: $pasteString, onDismiss: { + .fullScreenCover(item: $pasteString, onDismiss: { _setHash() }) { pasteString in NavigationStack { From ef91eb6f04a5faa3a5d551cf1e8b275da79b6999 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 17 Sep 2024 17:04:43 +0200 Subject: [PATCH 2/5] gp class --- .../Components/DateUpdateManagerView.swift | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift index c3198cc..3cdb16d 100644 --- a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift +++ b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift @@ -228,3 +228,88 @@ struct DatePickingViewWithFormat: View { } } } + +struct GroupStageDatePickingView: View { + let title: String + @Binding var startDate: Date + @Binding var currentDate: Date? + var duration: Int? + var validateAction: (() async -> ()) + + @State private var confirmFollowingScheduleUpdate: Bool = false + @State private var updatingInProgress: Bool = false + + var body: some View { + Section { + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + if confirmFollowingScheduleUpdate { + RowButtonView("Confirmer et modifier les matchs") { + updatingInProgress = true + await validateAction() + updatingInProgress = false + confirmFollowingScheduleUpdate = false + } + } + } header: { + Text(title) + } footer: { + if confirmFollowingScheduleUpdate && updatingInProgress == false { + FooterButtonView("Modifier juste l'horaire de la poule") { + currentDate = startDate + confirmFollowingScheduleUpdate = false + } + } else { + HStack { + Menu { + Button("de 30 minutes") { + startDate = startDate.addingTimeInterval(1800) + } + + Button("d'une heure") { + startDate = startDate.addingTimeInterval(3600) + } + + Button("à 9h") { + startDate = startDate.atNine() + } + + Button("à demain 9h") { + startDate = startDate.tomorrowAtNine + } + + if let duration { + Button("à la prochaine rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * 60) + } + Button("à la précédente rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * -60) + } + } + } label: { + Text("décaler") + .underline() + } + .buttonStyle(.borderless) + Spacer() + + if currentDate != nil { + FooterButtonView("retirer l'horaire bloqué") { + currentDate = nil + } + } else { + FooterButtonView("bloquer l'horaire") { + currentDate = startDate + } + } + } + .buttonStyle(.borderless) + } + } + .onChange(of: startDate) { + confirmFollowingScheduleUpdate = true + } + .headerProminence(.increased) + } +} From 09c401299022ec9e81a38e4edd0a2231ec903be7 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 17 Sep 2024 18:22:44 +0200 Subject: [PATCH 3/5] fix search stuff --- PadelClub/Data/AppSettings.swift | 66 +++++++++++- .../Data/Federal/FederalTournament.swift | 4 +- PadelClub/Views/Club/ClubSearchView.swift | 4 - PadelClub/Views/Club/ClubsView.swift | 2 - .../LoserBracketFromGroupStageView.swift | 1 - .../Navigation/Agenda/ActivityView.swift | 6 -- .../Agenda/TournamentLookUpView.swift | 102 ++++++++---------- .../CourtAvailabilitySettingsView.swift | 1 - .../Views/Planning/PlanningByCourtView.swift | 1 - PadelClub/Views/Planning/PlanningView.swift | 1 - .../Screen/InscriptionManagerView.swift | 3 - 11 files changed, 111 insertions(+), 80 deletions(-) diff --git a/PadelClub/Data/AppSettings.swift b/PadelClub/Data/AppSettings.swift index 6f08933..60be3e3 100644 --- a/PadelClub/Data/AppSettings.swift +++ b/PadelClub/Data/AppSettings.swift @@ -16,8 +16,48 @@ final class AppSettings: MicroStorable { var didCreateAccount: Bool = false var didRegisterAccount: Bool = false + //search tournament stuff + var tournamentCategories: Set + var tournamentLevels: Set + var tournamentAges: Set + var tournamentTypes: Set + var startDate: Date + var endDate: Date + var city: String + var distance: Double + var sortingOption: String + var nationalCup: Bool + var dayDuration: Int? + var dayPeriod: DayPeriod + + func resetSearch() { + tournamentAges = Set() + tournamentTypes = Set() + tournamentLevels = Set() + tournamentCategories = Set() + city = "" + distance = 30 + startDate = Date() + endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())! + sortingOption = "dateDebut+asc" + nationalCup = false + dayDuration = nil + dayPeriod = .all + } + required init() { - + tournamentAges = Set() + tournamentTypes = Set() + tournamentLevels = Set() + tournamentCategories = Set() + city = "" + distance = 30 + startDate = Date() + endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())! + sortingOption = "dateDebut+asc" + nationalCup = false + dayDuration = nil + dayPeriod = .all } required init(from decoder: Decoder) throws { @@ -25,11 +65,35 @@ final class AppSettings: MicroStorable { lastDataSource = try container.decodeIfPresent(String.self, forKey: ._lastDataSource) didCreateAccount = try container.decodeIfPresent(Bool.self, forKey: ._didCreateAccount) ?? false didRegisterAccount = try container.decodeIfPresent(Bool.self, forKey: ._didRegisterAccount) ?? false + tournamentCategories = try container.decodeIfPresent(Set.self, forKey: ._tournamentCategories) ?? Set() + tournamentLevels = try container.decodeIfPresent(Set.self, forKey: ._tournamentLevels) ?? Set() + tournamentAges = try container.decodeIfPresent(Set.self, forKey: ._tournamentAges) ?? Set() + tournamentTypes = try container.decodeIfPresent(Set.self, forKey: ._tournamentTypes) ?? Set() + startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? Date() + endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) ?? Calendar.current.date(byAdding: .month, value: 3, to: Date())! + city = try container.decodeIfPresent(String.self, forKey: ._city) ?? "" + distance = try container.decodeIfPresent(Double.self, forKey: ._distance) ?? 30 + sortingOption = try container.decodeIfPresent(String.self, forKey: ._sortingOption) ?? "dateDebut+asc" + nationalCup = try container.decodeIfPresent(Bool.self, forKey: ._nationalCup) ?? false + dayDuration = try container.decodeIfPresent(Int.self, forKey: ._dayDuration) + dayPeriod = try container.decodeIfPresent(DayPeriod.self, forKey: ._dayPeriod) ?? .all } enum CodingKeys: String, CodingKey { case _lastDataSource = "lastDataSource" case _didCreateAccount = "didCreateAccount" case _didRegisterAccount = "didRegisterAccount" + case _tournamentCategories = "tournamentCategories" + case _tournamentLevels = "tournamentLevels" + case _tournamentAges = "tournamentAges" + case _tournamentTypes = "tournamentTypes" + case _startDate = "startDate" + case _endDate = "endDate" + case _city = "city" + case _distance = "distance" + case _sortingOption = "sortingOption" + case _nationalCup = "nationalCup" + case _dayDuration = "dayDuration" + case _dayPeriod = "dayPeriod" } } diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index 16360ce..63595b5 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -7,8 +7,8 @@ import Foundation import CoreLocation import LeStorage -enum DayPeriod: CaseIterable, Identifiable { - var id: Self { self } +enum DayPeriod: Int, CaseIterable, Identifiable, Codable { + var id: Int { self.rawValue } case all case weekend diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index cda6c72..fc022f1 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -140,7 +140,6 @@ struct ClubSearchView: View { RowButtonView("D'accord") { locationManager.lastError = nil } - .padding(.horizontal) } } else if clubMarkers.isEmpty == false && searching == false && _filteredClubs().isEmpty { ContentUnavailableView.search(text: searchedCity) @@ -172,7 +171,6 @@ struct ClubSearchView: View { locationManager.requestLocation() } } - .padding(.horizontal) } if error != nil { @@ -184,13 +182,11 @@ struct ClubSearchView: View { RowButtonView("Chercher une ville ou un code postal") { searchPresented = true } - .padding(.horizontal) if searchAttempted { RowButtonView("Créer un club manuellement") { newClub = club ?? Club.newEmptyInstance() } - .padding(.horizontal) } } } diff --git a/PadelClub/Views/Club/ClubsView.swift b/PadelClub/Views/Club/ClubsView.swift index 041f4ca..b5ea93e 100644 --- a/PadelClub/Views/Club/ClubsView.swift +++ b/PadelClub/Views/Club/ClubsView.swift @@ -159,11 +159,9 @@ struct ClubsView: View { RowButtonView("Créer un nouveau club", systemImage: "plus.circle.fill") { newClub = Club.newEmptyInstance() } - .padding(.horizontal) RowButtonView("Chercher un club", systemImage: "magnifyingglass.circle.fill") { presentClubSearchView = true } - .padding(.horizontal) } } diff --git a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift index 08efa0e..776e75f 100644 --- a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift +++ b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift @@ -81,7 +81,6 @@ struct LoserBracketFromGroupStageView: View { isEditingLoserBracketGroupStage = true _addNewMatch() } - .padding(.horizontal) } } } diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 596b360..835c9cd 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -122,7 +122,6 @@ struct ActivityView: View { RowButtonView("D'accord.") { self.error = nil } - .padding(.horizontal) } } else if isGatheringFederalTournaments { ProgressView() @@ -139,11 +138,9 @@ struct ActivityView: View { FooterButtonView("supprimer vos filtres") { federalDataViewModel.removeFilters() } - .padding(.horizontal) FooterButtonView("modifier vos filtres") { presentFilterView = true } - .padding(.horizontal) } } else { _dataEmptyView() @@ -403,13 +400,10 @@ struct ActivityView: View { RowButtonView("Créer un nouvel événement") { newTournament = Tournament.newEmptyInstance() } - .padding(.horizontal) RowButtonView("Importer via Tenup") { navigation.agendaDestination = .tenup } - .padding(.horizontal) SupportButtonView(contentIsUnavailable: true) - .padding(.horizontal) } } diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index d4e8ca7..2f64f44 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -10,6 +10,7 @@ import CoreLocation import CoreLocationUI struct TournamentLookUpView: View { + @EnvironmentObject var dataStore: DataStore @Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel @StateObject var locationManager = LocationManager() @Environment(\.dismiss) private var dismiss @@ -18,19 +19,8 @@ struct TournamentLookUpView: View { @State var page: Int = 0 @State var total: Int = 0 - @State private var tournamentCategories = Set() - @State private var tournamentLevels = Set() - @State private var tournamentAges = Set() - @State private var tournamentTypes = Set() @State private var searching: Bool = false - @State private var startDate: Date = Date() - @State private var endDate: Date = Calendar.current.date(byAdding: .month, value: 3, to: Date())! - @AppStorage("lastCity") private var city: String = "" - @State private var ligue: String = "" - @State private var distance: Double = 30 - @State private var sortingOption: String = "dateDebut+asc" @State private var requestedToGetAllPages: Bool = false - @State private var nationalCup: Bool = false @State private var revealSearchParameters: Bool = true @State private var presentAlert: Bool = false @@ -61,14 +51,18 @@ struct TournamentLookUpView: View { presentAlert = false } }, message: { - Text("Il y a beacoup de tournois pour cette requête, êtes-vous sûr de vouloir tout récupérer ? Sinon essayez d'affiner votre recherche.") + if dataStore.appSettings.city.isEmpty { + Text("Il est préférable de se localiser ou d'indiquer une ville pour réduire le nombre de résultat.") + } else { + Text("Il y a beacoup de tournois pour cette requête, êtes-vous sûr de vouloir tout récupérer ? Sinon essayez d'affiner votre recherche.") + } }) .toolbarBackground(.visible, for: .bottomBar, .navigationBar) .navigationTitle("Chercher un tournoi") .navigationBarTitleDisplayMode(.inline) .onChange(of: locationManager.city) { - if let newValue = locationManager.city, city.isEmpty { - city = newValue + if let newValue = locationManager.city, dataStore.appSettings.city.isEmpty { + dataStore.appSettings.city = newValue } } .toolbarTitleDisplayMode(.large) @@ -113,15 +107,9 @@ struct TournamentLookUpView: View { #endif Button(role: .destructive) { - tournamentLevels = Set() - tournamentCategories = Set() - city = "" + dataStore.appSettings.resetSearch() locationManager.location = nil locationManager.city = nil - distance = 30 - startDate = Date() - endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())! - sortingOption = "dateDebut+asc" revealSearchParameters = true federalDataViewModel.searchedFederalTournaments = [] federalDataViewModel.searchAttemptCount = 0 @@ -151,6 +139,7 @@ struct TournamentLookUpView: View { } private func runSearch() { + dataStore.appSettingsStorage.write() revealSearchParameters = false total = 0 page = 0 @@ -158,6 +147,9 @@ struct TournamentLookUpView: View { searching = true requestedToGetAllPages = false federalDataViewModel.searchAttemptCount += 1 + federalDataViewModel.dayPeriod = dataStore.appSettings.dayPeriod + federalDataViewModel.dayDuration = dataStore.appSettings.dayDuration + Task { await getNewPage() searching = false @@ -170,7 +162,7 @@ struct TournamentLookUpView: View { } private var distanceLimit: Measurement { - distanceLimit(distance: distance) + distanceLimit(distance: dataStore.appSettings.distance) } private func distanceLimit(distance: Double) -> Measurement { @@ -178,19 +170,19 @@ struct TournamentLookUpView: View { } private var categories: [TournamentCategory] { - tournamentCategories.compactMap { TournamentCategory(rawValue: $0) } + dataStore.appSettings.tournamentCategories.compactMap { TournamentCategory(rawValue: $0) } } private var levels: [TournamentLevel] { - tournamentLevels.compactMap { TournamentLevel(rawValue: $0) } + dataStore.appSettings.tournamentLevels.compactMap { TournamentLevel(rawValue: $0) } } private var ages: [FederalTournamentAge] { - tournamentAges.compactMap { FederalTournamentAge(rawValue: $0) } + dataStore.appSettings.tournamentAges.compactMap { FederalTournamentAge(rawValue: $0) } } private var types: [FederalTournamentType] { - tournamentTypes.compactMap { FederalTournamentType(rawValue: $0) } + dataStore.appSettings.tournamentTypes.compactMap { FederalTournamentType(rawValue: $0) } } func getNewPage() async { @@ -198,7 +190,7 @@ struct TournamentLookUpView: View { if NetworkFederalService.shared.formId.isEmpty { await getNewBuildForm() } else { - let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: sortingOption, page: page, startDate: startDate, endDate: endDate, city: city, distance: distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: nationalCup) + let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: dataStore.appSettings.sortingOption, page: page, startDate: dataStore.appSettings.startDate, endDate: dataStore.appSettings.endDate, city: dataStore.appSettings.city, distance: dataStore.appSettings.distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: dataStore.appSettings.nationalCup) let resultCommand = commands.first(where: { $0.results != nil }) if let newTournaments = resultCommand?.results?.items { newTournaments.forEach { ft in @@ -253,15 +245,9 @@ struct TournamentLookUpView: View { } } Button { - tournamentLevels = Set() - tournamentCategories = Set() - city = "" + dataStore.appSettings.resetSearch() locationManager.location = nil locationManager.city = nil - distance = 30 - startDate = Date() - endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())! - sortingOption = "dateDebut+asc" revealSearchParameters = true } label: { Label("Ré-initialiser la recherche", systemImage: "xmark.circle") @@ -271,12 +257,12 @@ struct TournamentLookUpView: View { @ViewBuilder var searchParametersView: some View { - @Bindable var federalDataViewModel = federalDataViewModel + @Bindable var appSettings = dataStore.appSettings Section { - DatePicker("Début", selection: $startDate, displayedComponents: .date) - DatePicker("Fin", selection: $endDate, displayedComponents: .date) - Picker(selection: $federalDataViewModel.dayDuration) { - Text("aucune").tag(nil as Int?) + DatePicker("Début", selection: $appSettings.startDate, displayedComponents: .date) + DatePicker("Fin", selection: $appSettings.endDate, displayedComponents: .date) + Picker(selection: $appSettings.dayDuration) { + Text("Aucune").tag(nil as Int?) Text(1.formatted()).tag(1 as Int?) Text(2.formatted()).tag(2 as Int?) Text(3.formatted()).tag(3 as Int?) @@ -284,16 +270,16 @@ struct TournamentLookUpView: View { Text("Durée max (en jours)") } - Picker(selection: $federalDataViewModel.dayPeriod) { + Picker(selection: $appSettings.dayPeriod) { ForEach(DayPeriod.allCases) { - Text($0.localizedDayPeriodLabel()).tag($0) + Text($0.localizedDayPeriodLabel().capitalized).tag($0) } } label: { Text("En semaine ou week-end") } HStack { - TextField("Ville", text: $city) + TextField("Ville", text: $appSettings.city) if let city = locationManager.city { Divider() Text(city).italic() @@ -311,7 +297,7 @@ struct TournamentLookUpView: View { } } - Picker(selection: $distance) { + Picker(selection: $appSettings.distance) { Text(distanceLimit(distance:30).formatted()).tag(30.0) Text(distanceLimit(distance:50).formatted()).tag(50.0) Text(distanceLimit(distance:60).formatted()).tag(60.0) @@ -324,7 +310,7 @@ struct TournamentLookUpView: View { Text("Distance max") } - Picker(selection: $sortingOption) { + Picker(selection: $appSettings.sortingOption) { Text("Distance").tag("_DIST_") Text("Date de début").tag("dateDebut+asc") Text("Date de fin").tag("dateFin+asc") @@ -333,7 +319,7 @@ struct TournamentLookUpView: View { } NavigationLink { - List(TournamentCategory.allCases, selection: $tournamentCategories) { type in + List([TournamentCategory.men, TournamentCategory.women, TournamentCategory.mix], selection: $appSettings.tournamentCategories) { type in Text(type.localizedLabel()) } .navigationTitle("Catégories") @@ -348,7 +334,7 @@ struct TournamentLookUpView: View { } NavigationLink { - List(TournamentLevel.allCases, selection: $tournamentLevels) { type in + List([TournamentLevel.p25, TournamentLevel.p100, TournamentLevel.p250, TournamentLevel.p500, TournamentLevel.p1000, TournamentLevel.p1500, TournamentLevel.p2000], selection: $appSettings.tournamentLevels) { type in Text(type.localizedLabel()) } .navigationTitle("Niveaux") @@ -363,7 +349,7 @@ struct TournamentLookUpView: View { } NavigationLink { - List(FederalTournamentAge.allCases, selection: $tournamentAges) { type in + List([FederalTournamentAge.senior, FederalTournamentAge.a45, FederalTournamentAge.a55, FederalTournamentAge.a17_18, FederalTournamentAge.a15_16, FederalTournamentAge.a13_14, FederalTournamentAge.a11_12], selection: $appSettings.tournamentAges) { type in Text(type.localizedLabel()) } .navigationTitle("Limites d'âge") @@ -372,7 +358,7 @@ struct TournamentLookUpView: View { HStack { Text("Limite d'âge") Spacer() - if tournamentAges.isEmpty || tournamentAges.count == FederalTournamentAge.allCases.count { + if dataStore.appSettings.tournamentAges.isEmpty || dataStore.appSettings.tournamentAges.count == FederalTournamentAge.allCases.count { Text("Tous les âges") .foregroundStyle(.secondary) } else { @@ -383,7 +369,7 @@ struct TournamentLookUpView: View { } NavigationLink { - List(FederalTournamentType.allCases, selection: $tournamentTypes) { type in + List(FederalTournamentType.allCases, selection: $appSettings.tournamentTypes) { type in Text(type.localizedLabel()) } .navigationTitle("Types de tournoi") @@ -392,7 +378,7 @@ struct TournamentLookUpView: View { HStack { Text("Type de tournoi") Spacer() - if tournamentTypes.isEmpty || tournamentTypes.count == FederalTournamentType.allCases.count { + if dataStore.appSettings.tournamentTypes.isEmpty || dataStore.appSettings.tournamentTypes.count == FederalTournamentType.allCases.count { Text("Tous les types") .foregroundStyle(.secondary) } else { @@ -402,7 +388,7 @@ struct TournamentLookUpView: View { } } - Picker(selection: $nationalCup) { + Picker(selection: $appSettings.nationalCup) { Text("N'importe").tag(false) Text("Uniquement").tag(true) } label: { @@ -416,7 +402,7 @@ struct TournamentLookUpView: View { } var categoriesLabel: some View { - if tournamentCategories.isEmpty || tournamentCategories.count == TournamentCategory.allCases.count { + if dataStore.appSettings.tournamentCategories.isEmpty || dataStore.appSettings.tournamentCategories.count == TournamentCategory.allCases.count { Text("Toutes les catégories") } else { Text(categories.map({ $0.localizedLabel() }).joined(separator: ", ")) @@ -424,7 +410,7 @@ struct TournamentLookUpView: View { } var levelsLabel: some View { - if tournamentLevels.isEmpty || tournamentLevels.count == TournamentLevel.allCases.count { + if dataStore.appSettings.tournamentLevels.isEmpty || dataStore.appSettings.tournamentLevels.count == TournamentLevel.allCases.count { Text("Tous les niveaux") } else { Text(levels.map({ $0.localizedLabel() }).joined(separator: ", ")) @@ -437,8 +423,8 @@ struct TournamentLookUpView: View { HStack { Text("Lieu") Spacer() - Text(city) - if distance >= 3000 { + Text(dataStore.appSettings.city) + if dataStore.appSettings.distance >= 3000 { Text("sans limite de distance") } else { Text("à moins de " + distanceLimit.formatted()) @@ -448,9 +434,9 @@ struct TournamentLookUpView: View { Text("Période") Spacer() Text("Du") - Text(startDate.twoDigitsYearFormatted) + Text(dataStore.appSettings.startDate.twoDigitsYearFormatted) Text("Au") - Text(endDate.twoDigitsYearFormatted) + Text(dataStore.appSettings.endDate.twoDigitsYearFormatted) } HStack { Text("Niveau") @@ -472,7 +458,7 @@ struct TournamentLookUpView: View { } var sortingOptionLabel: String { - switch sortingOption { + switch dataStore.appSettings.sortingOption { case "_DIST_": return "Distance" case "dateDebut+asc": return "Date de début" case "dateFin+asc": return "Date de fin" diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index 31b14ea..9ade115 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -114,7 +114,6 @@ struct CourtAvailabilitySettingsView: View { endDate = tournament.startDate.addingTimeInterval(5400) showingPopover = true } - .padding(.horizontal) } } } diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index ba1bdef..96302fd 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -51,7 +51,6 @@ struct PlanningByCourtView: View { RowButtonView("Horaire intelligent") { selectedScheduleDestination = nil } - .padding(.horizontal) } } } diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index 1e0c0fc..e490a08 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -40,7 +40,6 @@ struct PlanningView: View { RowButtonView("Horaire intelligent") { selectedScheduleDestination = nil } - .padding(.horizontal) } } } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index e656aea..58f26f4 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -224,14 +224,11 @@ struct InscriptionManagerView: View { RowButtonView("Ajouter une équipe") { presentAddTeamView = true } - .padding(.horizontal) RowButtonView("Importer un fichier") { presentImportView = true } - .padding(.horizontal) } - .padding() } } } From 393069ec459eb385f941a6e6c055604927d5e3eb Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 17 Sep 2024 21:12:32 +0200 Subject: [PATCH 4/5] fix stuff in file import --- PadelClub.xcodeproj/project.pbxproj | 2 ++ PadelClub/Data/PlayerRegistration.swift | 4 ++-- PadelClub/Views/Components/Labels.swift | 2 +- .../Player/Components/PlayerPopoverView.swift | 10 +++++----- PadelClub/Views/Player/PlayerDetailView.swift | 2 ++ PadelClub/Views/Team/EditingTeamView.swift | 2 +- PadelClub/Views/Tournament/FileImportView.swift | 16 +++++++++++++--- .../Views/Tournament/Screen/AddTeamView.swift | 9 ++++++--- .../Screen/TournamentSettingsView.swift | 2 +- .../Views/Tournament/TournamentInitView.swift | 4 ++-- 10 files changed, 35 insertions(+), 18 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index a506e45..59d9ba6 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3156,6 +3156,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -3198,6 +3199,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index b53db2f..da256cf 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -160,8 +160,8 @@ final class PlayerRegistration: ModelObject, Storable { } func isSameAs(_ player: PlayerRegistration) -> Bool { - firstName.localizedCaseInsensitiveCompare(player.firstName) == .orderedSame && - lastName.localizedCaseInsensitiveCompare(player.lastName) == .orderedSame + firstName.trimmedMultiline.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline) == .orderedSame && + lastName.trimmedMultiline.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline) == .orderedSame } func tournament() -> Tournament? { diff --git a/PadelClub/Views/Components/Labels.swift b/PadelClub/Views/Components/Labels.swift index 5d2ed00..7049334 100644 --- a/PadelClub/Views/Components/Labels.swift +++ b/PadelClub/Views/Components/Labels.swift @@ -21,7 +21,7 @@ struct LabelStructure: View { struct LabelSettings: View { var body: some View { - Label("Réglages", systemImage: "slider.horizontal.3").labelStyle(.titleOnly) + Label("Réglages du tournoi", systemImage: "slider.horizontal.3").labelStyle(.titleOnly) } } diff --git a/PadelClub/Views/Player/Components/PlayerPopoverView.swift b/PadelClub/Views/Player/Components/PlayerPopoverView.swift index 55ff56c..f1dbe6e 100644 --- a/PadelClub/Views/Player/Components/PlayerPopoverView.swift +++ b/PadelClub/Views/Player/Components/PlayerPopoverView.swift @@ -99,7 +99,7 @@ struct PlayerPopoverView: View { .textInputAutocapitalization(.words) .focused($firstNameIsFocused) .onSubmit { - firstName = firstName.trimmed + firstName = firstName.trimmedMultiline lastNameIsFocused = true } .fixedSize() @@ -114,7 +114,7 @@ struct PlayerPopoverView: View { .keyboardType(.alphabet) .focused($lastNameIsFocused) .onSubmit { - lastName = lastName.trimmed + lastName = lastName.trimmedMultiline licenseIsFocused = true } .fixedSize() @@ -128,7 +128,7 @@ struct PlayerPopoverView: View { .keyboardType(.numberPad) .submitLabel(.next) .onSubmit { - license = license.trimmed + license = license.trimmedMultiline if requiredField.contains(.license) { if license.isLicenseNumber { amountIsFocused = true @@ -206,7 +206,7 @@ struct PlayerPopoverView: View { Spacer() Button("Confirmer") { if licenseIsFocused { - license = license.trimmed + license = license.trimmedMultiline if requiredField.contains(.license) { if license.isLicenseNumber { amountIsFocused = true @@ -251,7 +251,7 @@ struct PlayerPopoverView: View { } func createManualPlayer() { - let playerRegistration = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: license.trimmed.isEmpty ? nil : license, rank: rank, sex: PlayerRegistration.PlayerSexType(rawValue: sex)) + let playerRegistration = PlayerRegistration(firstName: firstName.trimmedMultiline, lastName: lastName.trimmedMultiline, licenceId: license.trimmedMultiline.isEmpty ? nil : license, rank: rank, sex: PlayerRegistration.PlayerSexType(rawValue: sex)) self.creationCompletionHandler(playerRegistration) } diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 938a7ac..3d65b72 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -41,6 +41,7 @@ struct PlayerDetailView: View { .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) .onSubmit(of: .text) { + player.lastName = player.lastName.trimmedMultiline _save() } } label: { @@ -53,6 +54,7 @@ struct PlayerDetailView: View { .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) .onSubmit(of: .text) { + player.firstName = player.firstName.trimmedMultiline _save() } } label: { diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 2c017db..bee37c6 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -147,7 +147,7 @@ struct EditingTeamView: View { .frame(maxWidth: .infinity) .submitLabel(.done) .onSubmit(of: .text) { - let trimmed = name.trimmed + let trimmed = name.trimmedMultiline if trimmed.isEmpty { team.name = nil } else { diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index f7e9c42..8147fdf 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -311,6 +311,11 @@ struct FileImportView: View { } label: { Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) \(tournament.federalTournamentAge.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)") } + LabeledContent { + Text(_filteredTeams.count.formatted()) + } label: { + Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) \(tournament.federalTournamentAge.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)") + } } footer: { if previousTeams.isEmpty == false { Text("La liste ci-dessous n'est qu'une indication d'évolution par rapport au seul poids d'équipe. Cela ne tient pas compte des dates d'inscriptions, WCs et autres éléments.").foregroundStyle(.logoRed) @@ -442,11 +447,16 @@ struct FileImportView: View { .disabled(validationInProgress) } + private func _getUnfound(tournament: Tournament, fromTeams filteredTeams: [FileImportManager.TeamHolder]) -> Set { + let previousTeams = filteredTeams.compactMap({ $0.previousTeam }) + let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams)) + return unfound + } + private func _validate(tournament: Tournament) async { let filteredTeams = filteredTeams(tournament: tournament) - let previousTeams = filteredTeams.compactMap({ $0.previousTeam }) - let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams)) + let unfound = _getUnfound(tournament: tournament, fromTeams: filteredTeams) unfound.forEach { team in team.resetPositions() team.wildCardBracket = false @@ -455,7 +465,7 @@ struct FileImportView: View { } do { - try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unfound) + try tournament.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unfound) } catch { Logger.error(error) } diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 9b200ae..01575a4 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -164,7 +164,7 @@ struct AddTeamView: View { } } } - .navigationBarBackButtonHidden(_isEditingTeam()) + .navigationBarBackButtonHidden(true) .toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .bottomBar) .navigationBarTitleDisplayMode(.inline) @@ -336,6 +336,7 @@ struct AddTeamView: View { if let pasteString { Section { TextEditor(text: $editableTextField) + .frame(minHeight: 120, maxHeight: .infinity) .focused($focusedField, equals: .pasteField) .toolbar { ToolbarItemGroup(placement: .keyboard) { @@ -355,15 +356,17 @@ struct AddTeamView: View { Text("Contenu du presse-papier") } footer: { HStack { + FooterButtonView("éditer") { + self.focusedField = .pasteField + } Spacer() - Button("effacer", role: .destructive) { + FooterButtonView("effacer", role: .destructive) { self.focusedField = nil self.editableTextField = "" self.pasteString = nil self.createdPlayers.removeAll() self.createdPlayerIds.removeAll() } - .buttonStyle(.borderless) } } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift index 98837ad..5dc380e 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift @@ -74,7 +74,7 @@ struct TournamentSettingsView: View { } .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) - .navigationTitle("Réglages") + .navigationTitle("Réglages du tournoi") } } diff --git a/PadelClub/Views/Tournament/TournamentInitView.swift b/PadelClub/Views/Tournament/TournamentInitView.swift index 969f6ff..2f36a05 100644 --- a/PadelClub/Views/Tournament/TournamentInitView.swift +++ b/PadelClub/Views/Tournament/TournamentInitView.swift @@ -40,7 +40,7 @@ struct TournamentInitView: View { Image(systemName: "checkmark").foregroundStyle(.green) } } label: { - LabelStructure() + Text("Structure") Text(tournament.structureDescriptionLocalizedLabel()) } } @@ -49,7 +49,7 @@ struct TournamentInitView: View { LabeledContent { Text(tournament.localizedTournamentType()) } label: { - LabelSettings() + Text("Réglages du tournoi") Text("Formats, terrains, prix et plus") } } From 19b413ff3e59807d6fb54ae079195adf9ed0f033 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 18 Sep 2024 11:50:58 +0200 Subject: [PATCH 5/5] add loser bracket mode --- PadelClub/Data/Round.swift | 51 +- PadelClub/Data/Tournament.swift | 13 +- PadelClub/Data/User.swift | 41 +- .../Views/Navigation/Umpire/UmpireView.swift | 16 + .../Views/Round/LoserRoundSettingsView.swift | 27 + .../Shared/SelectablePlayerListView.swift | 591 +++++++++++++++++- .../Views/Tournament/Screen/AddTeamView.swift | 4 +- .../TournamentGeneralSettingsView.swift | 49 +- 8 files changed, 755 insertions(+), 37 deletions(-) diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 6fd9425..6f73258 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -23,14 +23,16 @@ final class Round: ModelObject, Storable { private(set) var format: MatchFormat? var startDate: Date? var groupStageLoserBracket: Bool = false - - internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false) { + var loserBracketMode: LoserBracketMode = .automatic + + internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { self.tournament = tournament self.index = index self.parent = parent self.format = matchFormat self.startDate = startDate self.groupStageLoserBracket = groupStageLoserBracket + self.loserBracketMode = loserBracketMode } // MARK: - Computed dependencies @@ -214,7 +216,7 @@ defer { } #endif let parentRound = parentRound - if parentRound?.parent == nil, groupStageLoserBracket == false, tournamentObject()?.automaticLoserBracket == false { + if let parentRound, parentRound.parent == nil, groupStageLoserBracket == false, parentRound.loserBracketMode != .automatic { return nil } @@ -235,7 +237,7 @@ defer { #endif let parentRound = parentRound - if parentRound?.parent == nil, groupStageLoserBracket == false, tournamentObject()?.automaticLoserBracket == false { + if let parentRound, parentRound.parent == nil, groupStageLoserBracket == false, parentRound.loserBracketMode != .automatic { return nil } @@ -435,6 +437,10 @@ defer { return _matches().filter({ $0.disabled }) } + func allLoserRoundMatches() -> [Match] { + loserRoundsAndChildren().flatMap({ $0._matches() }) + } + var theoryCumulativeMatchCount: Int { var totalMatches = RoundRule.numberOfMatches(forRoundIndex: index) if let parentRound { @@ -591,7 +597,11 @@ defer { func deleteLoserBracket() { do { - try self.tournamentStore.rounds.delete(contentOfs: loserRounds()) + let loserRounds = loserRounds() + for loserRound in loserRounds { + try loserRound.deleteDependencies() + } + try self.tournamentStore.rounds.delete(contentOfs: loserRounds) } catch { Logger.error(error) } @@ -696,6 +706,7 @@ defer { case _format = "format" case _startDate = "startDate" case _groupStageLoserBracket = "groupStageLoserBracket" + case _loserBracketMode = "loserBracketMode" } required init(from decoder: Decoder) throws { @@ -707,6 +718,7 @@ defer { format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) groupStageLoserBracket = try container.decodeIfPresent(Bool.self, forKey: ._groupStageLoserBracket) ?? false + loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic } func encode(to encoder: Encoder) throws { @@ -716,7 +728,8 @@ defer { try container.encode(tournament, forKey: ._tournament) try container.encode(index, forKey: ._index) try container.encode(groupStageLoserBracket, forKey: ._groupStageLoserBracket) - + try container.encode(loserBracketMode, forKey: ._loserBracketMode) + if let parent = parent { try container.encode(parent, forKey: ._parent) } else { @@ -791,3 +804,29 @@ extension Round: Selectable, Equatable { return hasEnded() ? .checkmark : nil } } + + +enum LoserBracketMode: Int, CaseIterable, Identifiable, Codable { + var id: Int { self.rawValue } + + case automatic + case manual + + func localizedLoserBracketMode() -> String { + switch self { + case .automatic: + "Automatique" + case .manual: + "Manuelle" + } + } + + func localizedLoserBracketModeDescription() -> String { + switch self { + case .automatic: + "Les perdants du tableau principal sont placés à leur place prévue." + case .manual: + "Aucun placement automatique n'est fait. Vous devez choisir les perdants qui se jouent." + } + } +} diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 3f478da..14437e0 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -57,8 +57,8 @@ final class Tournament : ModelObject, Storable { var publishTournament: Bool = false var hidePointsEarned: Bool = false var publishRankings: Bool = false - var automaticLoserBracket: Bool = true - + var loserBracketMode: LoserBracketMode = .automatic + @ObservationIgnored var navigationPath: [Screen] = [] @@ -106,9 +106,10 @@ final class Tournament : ModelObject, Storable { case _publishTournament = "publishTournament" case _hidePointsEarned = "hidePointsEarned" case _publishRankings = "publishRankings" + case _loserBracketMode = "loserBracketMode" } - internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false) { + internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { self.event = event self.name = name self.startDate = startDate @@ -146,6 +147,7 @@ final class Tournament : ModelObject, Storable { self.publishTournament = publishTournament self.hidePointsEarned = hidePointsEarned self.publishRankings = publishRankings + self.loserBracketMode = loserBracketMode } required init(from decoder: Decoder) throws { @@ -190,6 +192,7 @@ final class Tournament : ModelObject, Storable { publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false + loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic } fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter() @@ -1634,7 +1637,7 @@ defer { let roundCount = RoundRule.numberOfRounds(forTeams: bracketTeamCount()) let rounds = (0.. Tournament { diff --git a/PadelClub/Data/User.swift b/PadelClub/Data/User.swift index d40e126..59e3e1f 100644 --- a/PadelClub/Data/User.swift +++ b/PadelClub/Data/User.swift @@ -43,16 +43,18 @@ class User: ModelObject, UserBase, Storable { var bracketMatchFormatPreference: MatchFormat? var groupStageMatchFormatPreference: MatchFormat? var loserBracketMatchFormatPreference: MatchFormat? - + var loserBracketMode: LoserBracketMode = .automatic + var deviceId: String? - init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?) { + init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?, loserBracketMode: LoserBracketMode = .automatic) { self.username = username self.firstName = firstName self.lastName = lastName self.email = email self.phone = phone self.country = country + self.loserBracketMode = loserBracketMode } public func uuid() throws -> UUID { @@ -139,8 +141,42 @@ class User: ModelObject, UserBase, Storable { case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference" case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference" case _deviceId = "deviceId" + case _loserBracketMode = "loserBracketMode" } + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + // Required properties + id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() + username = try container.decode(String.self, forKey: ._username) + email = try container.decode(String.self, forKey: ._email) + firstName = try container.decode(String.self, forKey: ._firstName) + lastName = try container.decode(String.self, forKey: ._lastName) + + // Optional properties + clubs = try container.decodeIfPresent([String].self, forKey: ._clubs) ?? [] + umpireCode = try container.decodeIfPresent(String.self, forKey: ._umpireCode) + licenceId = try container.decodeIfPresent(String.self, forKey: ._licenceId) + phone = try container.decodeIfPresent(String.self, forKey: ._phone) + country = try container.decodeIfPresent(String.self, forKey: ._country) + + // Summons-related properties + summonsMessageBody = try container.decodeIfPresent(String.self, forKey: ._summonsMessageBody) + summonsMessageSignature = try container.decodeIfPresent(String.self, forKey: ._summonsMessageSignature) + summonsAvailablePaymentMethods = try container.decodeIfPresent(String.self, forKey: ._summonsAvailablePaymentMethods) + summonsDisplayFormat = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayFormat) ?? false + summonsDisplayEntryFee = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayEntryFee) ?? false + summonsUseFullCustomMessage = try container.decodeIfPresent(Bool.self, forKey: ._summonsUseFullCustomMessage) ?? false + + // Match-related properties + matchFormatsDefaultDuration = try container.decodeIfPresent([MatchFormat: Int].self, forKey: ._matchFormatsDefaultDuration) + bracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._bracketMatchFormatPreference) + groupStageMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageMatchFormatPreference) + loserBracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserBracketMatchFormatPreference) + loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic + } + func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -228,6 +264,7 @@ class User: ModelObject, UserBase, Storable { try container.encodeNil(forKey: ._deviceId) } + try container.encode(loserBracketMode, forKey: ._loserBracketMode) } static func placeHolder() -> User { diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index 0740e0b..70d778e 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -121,6 +121,22 @@ struct UmpireView: View { } + Section { + @Bindable var user = dataStore.user + Picker(selection: $user.loserBracketMode) { + ForEach(LoserBracketMode.allCases) { + Text($0.localizedLoserBracketMode()).tag($0) + } + } label: { + Text("Position des perdants") + } + .onChange(of: user.loserBracketMode) { + dataStore.saveUser() + } + } header: { + Text("Matchs de classement") + } + Section { NavigationLink { GlobalSettingsView() diff --git a/PadelClub/Views/Round/LoserRoundSettingsView.swift b/PadelClub/Views/Round/LoserRoundSettingsView.swift index 2e24481..08952fe 100644 --- a/PadelClub/Views/Round/LoserRoundSettingsView.swift +++ b/PadelClub/Views/Round/LoserRoundSettingsView.swift @@ -22,6 +22,28 @@ struct LoserRoundSettingsView: View { } } + Section { + @Bindable var round: Round = upperBracketRound.round + Picker(selection: $round.loserBracketMode) { + ForEach(LoserBracketMode.allCases) { + Text($0.localizedLoserBracketMode()).tag($0) + } + } label: { + Text("Position des perdants") + } + .onChange(of: round.loserBracketMode) { + do { + try self.tournament.tournamentStore.rounds.addOrUpdate(instance: upperBracketRound.round) + } catch { + Logger.error(error) + } + } + } header: { + Text("Matchs de classement") + } footer: { + Text(upperBracketRound.round.loserBracketMode.localizedLoserBracketModeDescription()) + } + Section { RowButtonView("Synchroniser les noms des matchs") { let allRoundMatches = upperBracketRound.loserRounds.flatMap({ $0.allMatches @@ -48,6 +70,11 @@ struct LoserRoundSettingsView: View { upperBracketRound.round.disabledMatches().forEach { match in match.disableMatch() } + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: upperBracketRound.round.allLoserRoundMatches()) + } catch { + Logger.error(error) + } } } .disabled(upperBracketRound.round.loserRounds().isEmpty == false) diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index b5d9a2d..7ba50a3 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -172,7 +172,7 @@ struct SelectablePlayerListView: View { } .scrollDismissesKeyboard(.immediately) .navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection) - .toolbarBackground(.visible, for: .bottomBar) + //.toolbarBackground(.visible, for: .bottomBar) // .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor) .interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false) .navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet)) @@ -358,13 +358,109 @@ struct MySearchView: View { @ViewBuilder var playersView: some View { + let showProgression = true + let showFemaleInMaleAssimilation = searchViewModel.showFemaleInMaleAssimilation if searchViewModel.allowMultipleSelection { List(selection: $searchViewModel.selectedPlayers) { if searchViewModel.filterSelectionEnabled { let array = Array(searchViewModel.selectedPlayers) Section { ForEach(array) { player in - ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + let index : Int? = nil + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + 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 { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } .onDelete { indexSet in for index in indexSet { @@ -379,7 +475,102 @@ struct MySearchView: View { } else { Section { ForEach(players, id: \.self) { player in - ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + let index : Int? = nil + + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + 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 { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } } header: { if players.isEmpty == false { @@ -395,10 +586,105 @@ struct MySearchView: View { if searchViewModel.allowSingleSelection { Section { ForEach(players) { player in + let index : Int? = nil + Button { searchViewModel.selectedPlayers.insert(player) } label: { - ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + 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 { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } .buttonStyle(.plain) } @@ -410,9 +696,104 @@ struct MySearchView: View { .id(UUID()) } else { Section { - ForEach(players.indices, id: \.self) { index in - let player = players[index] - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + ForEach(players.indices, id: \.self) { playerIndex in + let player = players[playerIndex] + let index: Int? = searchViewModel.showIndex() ? (playerIndex + 1) : nil + + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + 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 { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } } header: { if players.isEmpty == false { @@ -423,19 +804,205 @@ struct MySearchView: View { } } else { Section { - ForEach(players.indices, id: \.self) { index in - let player = players[index] + ForEach(players.indices, id: \.self) { playerIndex in + let player = players[playerIndex] + let index: Int? = searchViewModel.showIndex() ? (playerIndex + 1) : nil if searchViewModel.allowSingleSelection { Button { searchViewModel.selectedPlayers.insert(player) } label: { - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) - .contentShape(Rectangle()) + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + 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 { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } .frame(maxWidth: .infinity) .buttonStyle(.plain) } else { - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + 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 { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } } } header: { diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 01575a4..32b0f1a 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -72,11 +72,11 @@ struct AddTeamView: View { } var body: some View { - if pasteString == nil { + if pasteString != nil, fetchPlayers.isEmpty == false { computedBody + .searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher dans les résultats")) } else { computedBody - .searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .automatic), prompt: Text("Chercher dans les résultats")) } } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 11416ea..35afb65 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -35,19 +35,42 @@ struct TournamentGeneralSettingsView: View { } Section { - Toggle(isOn: $tournament.automaticLoserBracket) { - Text("Gestion automatique des matchs de classements") + Picker(selection: $tournament.loserBracketMode) { + ForEach(LoserBracketMode.allCases) { + Text($0.localizedLoserBracketMode()).tag($0) + } + } label: { + Text("Position des perdants") + } + .onChange(of: tournament.loserBracketMode) { + + _save() + + let rounds = tournament.rounds() + rounds.forEach { round in + round.loserBracketMode = tournament.loserBracketMode + } + + do { + try self.tournament.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) + } catch { + Logger.error(error) + } + } + } header: { + Text("Matchs de classement") + } footer: { + if dataStore.user.loserBracketMode != tournament.loserBracketMode { + _footerView() + .onTapGesture(perform: { + self.dataStore.user.loserBracketMode = tournament.loserBracketMode + self.dataStore.saveUser() + }) + } else { + Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) } -// Picker(selection: $tournament.loserBracketMode) { -// ForEach(LoserBracketMode.allCases) { mode in -// Text(mode.loserBracketModeLocalizedLabel()).tag(mode) -// } -// } label: { -// Text("Mode") -// } } - Section { LabeledContent { TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) @@ -132,4 +155,10 @@ struct TournamentGeneralSettingsView: View { Logger.error(error) } } + + private func _footerView() -> some View { + Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) + + + Text(" Modifier le réglage par défaut pour tous vos tournois").foregroundStyle(.blue) + } }