From 2f14a1685252d72d75f573c83fac9c5f43358de6 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 17 Sep 2024 09:47:16 +0200 Subject: [PATCH] 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 {