diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index c8d0568..7b57cc2 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -230,6 +230,7 @@ FFC91B012BD85C2F00B29808 /* Court.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC91B002BD85C2F00B29808 /* Court.swift */; }; FFC91B032BD85E2400B29808 /* CourtView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC91B022BD85E2400B29808 /* CourtView.swift */; }; FFCB74132C4625BB008384D0 /* GroupStageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCB74122C4625BB008384D0 /* GroupStageSettingsView.swift */; }; + FFCB74172C480411008384D0 /* CopyPasteButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCB74162C480411008384D0 /* CopyPasteButtonView.swift */; }; FFCD16B32C3E5E590092707B /* TeamsCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCD16B22C3E5E590092707B /* TeamsCallingView.swift */; }; FFCEDA4C2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCEDA4B2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift */; }; FFCF76072C3BE9BC006C8C3D /* CloseDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCF76062C3BE9BC006C8C3D /* CloseDatePicker.swift */; }; @@ -569,6 +570,7 @@ FFC91B002BD85C2F00B29808 /* Court.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Court.swift; sourceTree = ""; }; FFC91B022BD85E2400B29808 /* CourtView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourtView.swift; sourceTree = ""; }; FFCB74122C4625BB008384D0 /* GroupStageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageSettingsView.swift; sourceTree = ""; }; + FFCB74162C480411008384D0 /* CopyPasteButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyPasteButtonView.swift; sourceTree = ""; }; FFCD16B22C3E5E590092707B /* TeamsCallingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamsCallingView.swift; sourceTree = ""; }; FFCEDA4B2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayersWithoutContactView.swift; sourceTree = ""; }; FFCF76062C3BE9BC006C8C3D /* CloseDatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseDatePicker.swift; sourceTree = ""; }; @@ -810,6 +812,7 @@ C4FC2E262C2AABC90021F3BF /* PasswordField.swift */, FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */, C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */, + FFCB74162C480411008384D0 /* CopyPasteButtonView.swift */, ); path = Components; sourceTree = ""; @@ -1731,6 +1734,7 @@ C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */, FFC91B012BD85C2F00B29808 /* Court.swift in Sources */, FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, + FFCB74172C480411008384D0 /* CopyPasteButtonView.swift in Sources */, FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, FF1F4B712BF9EFE9000B4573 /* TournamentInscriptionView.swift in Sources */, FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */, diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index e3d0097..7a990d4 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -263,7 +263,7 @@ class DataStore: ObservableObject { var runningMatches: [Match] = [] for tournament in lastTournaments { let matches = tournament.tournamentStore.matches.filter { match in - match.confirmed && match.startDate != nil && match.endDate == nil && match.courtIndex != nil } + match.confirmed && match.startDate != nil && match.endDate == nil } runningMatches.append(contentsOf: matches) } return runningMatches diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index fb20842..7f8464b 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -66,6 +66,10 @@ final class Match: ModelObject, Storable { fatalError("missing store for \(String(describing: type(of: self)))") } + var courtIndexForSorting: Int { + courtIndex ?? Int.max + } + // MARK: - Computed dependencies var teamScores: [TeamScore] { @@ -474,8 +478,16 @@ defer { if startDate == nil { startDate = endDate?.addingTimeInterval(Double(-getDuration()*60)) } - winningTeamId = team(matchDescriptor.winner)?.id - losingTeamId = team(matchDescriptor.winner.otherTeam)?.id + + let teamOne = team(matchDescriptor.winner) + let teamTwo = team(matchDescriptor.winner.otherTeam) + + teamOne?.hasArrived() + teamTwo?.hasArrived() + + winningTeamId = teamOne?.id + losingTeamId = teamTwo?.id + groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() updateFollowingMatchTeamScore() diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 333ae94..bffff07 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1104,7 +1104,7 @@ defer { // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) } - func availableToStart(_ allMatches: [Match], in runningMatches: [Match]) async -> [Match] { + func availableToStart(_ allMatches: [Match], in runningMatches: [Match]) -> [Match] { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1115,17 +1115,6 @@ defer { return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting) } - func asyncRunningMatches(_ allMatches: [Match]) async -> [Match] { - #if DEBUG_TIME //DEBUGING TIME - let start = Date() - defer { - let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) - print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) - } - #endif - return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) - } - func runningMatches(_ allMatches: [Match]) -> [Match] { #if DEBUG_TIME //DEBUGING TIME let start = Date() @@ -1136,8 +1125,8 @@ defer { #endif return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) } - - func readyMatches(_ allMatches: [Match]) async -> [Match] { + + func readyMatches(_ allMatches: [Match]) -> [Match] { #if DEBUG_TIME //DEBUGING TIME let start = Date() defer { diff --git a/PadelClub/Views/Components/CopyPasteButtonView.swift b/PadelClub/Views/Components/CopyPasteButtonView.swift new file mode 100644 index 0000000..b3f4ee7 --- /dev/null +++ b/PadelClub/Views/Components/CopyPasteButtonView.swift @@ -0,0 +1,27 @@ +// +// CopyPasteButtonView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/07/2024. +// + +import SwiftUI + +struct CopyPasteButtonView: View { + let pasteValue: String? + @State private var copied: Bool = false + + var body: some View { + Button { + let pasteboard = UIPasteboard.general + pasteboard.string = pasteValue + } label: { + HStack(spacing: 0) { + Label(copied ? "copié" : "copier", systemImage: "doc.on.doc") + if copied == true { + Image(systemName: "checkmark") + } + } + } + } +} diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 0c1114b..999a564 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -107,16 +107,16 @@ struct GenericDestinationPickerView: } .onAppear { if let selectedDestination { - proxy.scrollTo(selectedDestination.id, anchor: .trailing) + proxy.scrollTo(selectedDestination.id, anchor: .center) } else { - proxy.scrollTo("settings", anchor: .leading) + proxy.scrollTo("settings", anchor: .center) } } .onChange(of: selectedDestination) { if let selectedDestination { - proxy.scrollTo(selectedDestination.id, anchor: .trailing) + proxy.scrollTo(selectedDestination.id, anchor: .center) } else { - proxy.scrollTo("settings", anchor: .leading) + proxy.scrollTo("settings", anchor: .center) } } } diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 1c87665..44211f2 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -15,12 +15,13 @@ struct GroupStageView: View { @Environment(Tournament.self) private var tournament @Bindable var groupStage: GroupStage @State private var confirmGroupStageStart: Bool = false - @State private var sortingMode: GroupStageSortingMode = .auto + @State private var sortingMode: GroupStageSortingMode let playedMatches: [Match] init(groupStage: GroupStage) { self.groupStage = groupStage self.playedMatches = groupStage.playedMatches() + _sortingMode = .init(wrappedValue: groupStage.hasEnded() ? .score : .weight) } var tournamentStore: TournamentStore { @@ -30,7 +31,7 @@ struct GroupStageView: View { var body: some View { List { Section { - GroupStageScoreView(groupStage: groupStage, sortByScore: _sortByScore) + GroupStageScoreView(groupStage: groupStage, sortByScore: sortingMode == .score) } header: { if let startDate = groupStage.startDate { Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formattedAsHourMinute()) @@ -38,13 +39,14 @@ struct GroupStageView: View { } footer: { HStack { Spacer() - Button { - _updateSortingMode() + Picker(selection: $sortingMode) { + Text("tri par score").tag(GroupStageSortingMode.score) + Text("tri par poids").tag(GroupStageSortingMode.weight) } label: { - Label(_sortByScore ? "tri par score" : "tri par poids", systemImage: "arrow.up.arrow.down").labelStyle(.titleOnly) - .underline() + } - .buttonStyle(.borderless) + .pickerStyle(.segmented) + Spacer() } } .headerProminence(.increased) @@ -66,29 +68,10 @@ struct GroupStageView: View { } private enum GroupStageSortingMode { - case auto case score case weight } - - private var _sortByScore: Bool { - sortingMode == .auto ? groupStage.hasEnded() : sortingMode == .score - } - - private func _updateSortingMode() { - if sortingMode == .auto { - if groupStage.hasEnded() { - sortingMode = .weight - } else { - sortingMode = .score - } - } else if sortingMode == .weight { - sortingMode = .score - } else { - sortingMode = .weight - } - } - + struct GroupStageScoreView: View { @EnvironmentObject var dataStore: DataStore @@ -142,7 +125,7 @@ struct GroupStageView: View { Text(player.playerLabel()).lineLimit(1) .overlay { if player.hasArrived && team.isHere() == false { - Color.logoYellow.opacity(0.6) + Color.green.opacity(0.6) } } } diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index 6ef48da..476bb03 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -20,27 +20,29 @@ struct GroupStagesSettingsView: View { var body: some View { List { - let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages() - Section { - let name = "\((tournament.groupStageAdditionalQualified + 1).ordinalFormatted())" - NavigationLink("Tirage au sort d'un \(name)") { - SpinDrawView(drawees: ["Qualification d'un \(name)"], segments: missingQualifiedFromGroupStages) { results in - results.forEach { drawResult in - missingQualifiedFromGroupStages[drawResult.drawIndex].qualified = true - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(instance: missingQualifiedFromGroupStages[drawResult.drawIndex]) - } catch { - Logger.error(error) + if tournament.groupStageAdditionalQualified > 0 { + let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages() + Section { + let name = "\((tournament.groupStageAdditionalQualified + 1).ordinalFormatted())" + NavigationLink("Tirage au sort d'un \(name)") { + SpinDrawView(drawees: ["Qualification d'un \(name)"], segments: missingQualifiedFromGroupStages) { results in + results.forEach { drawResult in + missingQualifiedFromGroupStages[drawResult.drawIndex].qualified = true + do { + try self.tournamentStore.teamRegistrations.addOrUpdate(instance: missingQualifiedFromGroupStages[drawResult.drawIndex]) + } catch { + Logger.error(error) + } } } } - } - .disabled(tournament.moreQualifiedToDraw() == 0 || missingQualifiedFromGroupStages.isEmpty) - } footer: { - if tournament.moreQualifiedToDraw() == 0 { - Text("Aucune équipe supplémentaire à qualifier. Vous pouvez en rajouter en modifier le paramètre dans structure.") - } else if missingQualifiedFromGroupStages.isEmpty { - Text("Aucune équipe supplémentaire à tirer au sort. Attendez la fin des poules.") + .disabled(tournament.moreQualifiedToDraw() == 0 || missingQualifiedFromGroupStages.isEmpty) + } footer: { + if tournament.moreQualifiedToDraw() == 0 { + Text("Aucune équipe supplémentaire à qualifier. Vous pouvez en rajouter en modifier le paramètre dans structure.") + } else if missingQualifiedFromGroupStages.isEmpty { + Text("Aucune équipe supplémentaire à tirer au sort. Attendez la fin des poules.") + } } } @@ -96,13 +98,7 @@ struct GroupStagesSettingsView: View { Text("Suite à un changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de vos poules et valider que tout est ok.") } } - - Section { - ShareLink(item: tournament.groupStages().compactMap { $0.pasteData() }.joined(separator: "\n\n")) { - Text("Exportez l'état poules") - } - } - + #if DEBUG Section { RowButtonView("delete all group stages") { @@ -192,6 +188,11 @@ struct GroupStagesSettingsView: View { .deferredRendering(for: .seconds(2)) } } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + ShareLink(item: tournament.groupStages().compactMap { $0.pasteData() }.joined(separator: "\n\n")) + } + } } diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index bd9a732..c2d1cdd 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -60,11 +60,12 @@ struct GroupStagesView: View { } } - let allMatches: [Match] + var allMatches: [Match] { + tournament.groupStagesMatches() + } init(tournament: Tournament) { self.tournament = tournament - self.allMatches = tournament.groupStagesMatches() if tournament.shouldVerifyGroupStage { _selectedDestination = State(wrappedValue: nil) @@ -85,10 +86,6 @@ struct GroupStagesView: View { return allDestinations } - @State private var runningMatches: [Match]? - @State private var readyMatches: [Match]? - @State private var availableToStart: [Match]? - var body: some View { VStack(spacing: 0) { GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) @@ -97,16 +94,12 @@ struct GroupStagesView: View { let finishedMatches = tournament.finishedMatches(allMatches) List { + let runningMatches = tournament.runningMatches(allMatches) MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "prêt à démarrer", matches: availableToStart, matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "à lancer", matches: readyMatches, matchViewStyle: .standardStyle, isExpanded: false) + MatchListView(section: "prêt à démarrer", matches: tournament.availableToStart(allMatches, in: runningMatches), matchViewStyle: .standardStyle, isExpanded: false) + MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), matchViewStyle: .standardStyle, isExpanded: false) MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false) } - .task { - runningMatches = await tournament.asyncRunningMatches(allMatches) - availableToStart = await tournament.availableToStart(allMatches, in: runningMatches ?? []) - readyMatches = await tournament.readyMatches(allMatches) - } .navigationTitle("Toutes les poules") case .groupStage(let groupStage): GroupStageView(groupStage: groupStage).id(groupStage.id) diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index e208067..5cdd1c1 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -144,6 +144,17 @@ struct MatchDateView: View { } private func _save() { + if let startDate = match.startDate, let tournament = match.currentTournament() { + if startDate < tournament.startDate { + tournament.startDate = startDate + } + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + do { try self.match.tournamentStore.matches.addOrUpdate(instance: match) } catch { diff --git a/PadelClub/Views/Match/EditSharingView.swift b/PadelClub/Views/Match/EditSharingView.swift index ff82dc3..33c0116 100644 --- a/PadelClub/Views/Match/EditSharingView.swift +++ b/PadelClub/Views/Match/EditSharingView.swift @@ -59,7 +59,6 @@ struct EditSharingView: View { var body: some View { List { - if let newImage { Section { let tip = SharePictureTip() @@ -87,12 +86,11 @@ struct EditSharingView: View { } } } else { - Button { - showCamera = true - } label: { - Label("Prendre une photo", systemImage: "camera") - } - + Section { + RowButtonView("Prendre une photo", systemImage: "camera") { + showCamera = true + } + } } Section { @@ -114,16 +112,7 @@ struct EditSharingView: View { } footer: { HStack { Spacer() - Button { - UIPasteboard.general.string = shareMessage - copied = true - } label: { - Label(copied ? "copié" : "copier", systemImage: "doc.on.doc") - } - - if shareMessage == UIPasteboard.general.string || copied == true { - Image(systemName: "checkmark") - } + CopyPasteButtonView(pasteValue: shareMessage) } } } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index ca7dd70..42a3ba9 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -491,6 +491,17 @@ struct MatchDetailView: View { } private func save() { + if let startDate = match.startDate, let tournament = match.currentTournament() { + if startDate < tournament.startDate { + tournament.startDate = startDate + } + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + do { try tournamentStore.matches.addOrUpdate(instance: match) } catch { diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index e0575ae..4c89482 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -104,10 +104,12 @@ struct EventListView: View { TournamentCellView(tournament: tournament) } .contextMenu { - Button { - navigation.openTournamentInOrganizer(tournament) - } label: { - Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") + if tournament.hasEnded() == false { + Button { + navigation.openTournamentInOrganizer(tournament) + } label: { + Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") + } } } #if DEBUG diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift index 736a97f..ff42385 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift @@ -15,8 +15,8 @@ struct OngoingView: View { @State private var sortByField: Bool = false - let fieldSorting : [MySortDescriptor] = [.keyPath(\Match.courtIndex!), .keyPath(\Match.startDate!)] - let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.startDate!), .keyPath(\Match.courtIndex!)] + let fieldSorting : [MySortDescriptor] = [.keyPath(\Match.courtIndexForSorting), .keyPath(\Match.startDate!)] + let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.startDate!), .keyPath(\Match.courtIndexForSorting)] var matches: [Match] { let sorting = self.sortByField ? fieldSorting : defaultSorting @@ -65,35 +65,17 @@ struct OngoingView: View { } } .navigationTitle("En cours") + .navigationBarTitleDisplayMode(.inline) .toolbar(matches.isEmpty ? .hidden : .visible, for: .navigationBar) .toolbar { - ToolbarItem(placement: .topBarLeading) { - Menu { - Picker(selection: $sortByField) { - Text("Trier par date").tag(false) - Text("Trier par terrain").tag(true) - } label: { - - } - #if DEBUG - Button("effacer les mauvais matchs (TODO)") { -// let bad = matches.filter({ $0.currentTournament() == nil }) -// do { -// try self.tournamentStore.matches.delete(contentOfs: bad) -// } catch { -// Logger.error(error) -// } - } - #endif - //todo - //presentFilterView.toggle() + ToolbarItem(placement: .principal) { + Picker(selection: $sortByField) { + Text("tri par date").tag(true) + Text("tri par terrain").tag(false) } label: { - Image(systemName: "line.3.horizontal.decrease.circle") - .resizable() - .scaledToFit() - .frame(minHeight: 28) + } - //.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none) + .pickerStyle(.segmented) } ToolbarItem(placement: .status) { if matches.isEmpty == false { diff --git a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift index 8479a91..499a2a0 100644 --- a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift +++ b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift @@ -32,15 +32,28 @@ struct TournamentOrganizerView: View { } Divider() HStack { - ScrollView(.horizontal) { - HStack { - let tournaments = dataStore.tournaments.filter({ $0.hasEnded() == false && $0.isDeleted == false && $0.isCanceled == false }).sorted(by: \.startDate).reversed() - ForEach(tournaments) { tournament in - TournamentButtonView(tournament: tournament) + ScrollViewReader { proxy in + ScrollView(.horizontal) { + HStack { + let tournaments = dataStore.tournaments.filter({ $0.hasEnded() == false && $0.isDeleted == false && $0.isCanceled == false }).sorted(by: \.startDate).reversed() + ForEach(tournaments) { tournament in + TournamentButtonView(tournament: tournament) + .id(tournament.id) + } + } + .padding() + .buttonStyle(.plain) + } + .onAppear { + if let selectedDestination = navigation.organizerTournament { + proxy.scrollTo(selectedDestination.id, anchor: .center) + } + } + .onChange(of: navigation.organizerTournament) { + if let selectedDestination = navigation.organizerTournament { + proxy.scrollTo(selectedDestination.id, anchor: .center) } } - .padding() - .buttonStyle(.plain) } } } diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index 1140880..4a7a55c 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -33,7 +33,7 @@ struct CourtAvailabilitySettingsView: View { ForEach(keys, id: \.self) { key in if let dates = courtsUnavailability[key] { Section { - ForEach(dates) { dateInterval in + ForEach(dates.sorted(by: \.startDate)) { dateInterval in Menu { Button("dupliquer") { let duplicatedDateInterval = DateInterval(event: event.id, courtIndex: (courtIndex+1)%tournament.courtCount, startDate: dateInterval.startDate, endDate: dateInterval.endDate) @@ -138,7 +138,18 @@ struct CourtAvailabilitySettingsView: View { Section { DatePicker("Début", selection: $startDate) + .onChange(of: startDate) { + if endDate < startDate { + endDate = startDate.addingTimeInterval(90*60) + } + } DatePicker("Fin", selection: $endDate) + .onChange(of: endDate) { + if startDate > endDate { + startDate = endDate.addingTimeInterval(-90*60) + } + + } } footer: { FooterButtonView("jour entier") { startDate = startDate.startOfDay diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift index e2e6d13..4f4dccb 100644 --- a/PadelClub/Views/Player/Components/EditablePlayerView.swift +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -60,7 +60,14 @@ struct EditablePlayerView: View { } } } - + .onChange(of: editedLicenceId) { + self.player.licenceId = editedLicenceId + do { + try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) + } catch { + Logger.error(error) + } + } } @ViewBuilder diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index ad1ceea..70dd10e 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -119,15 +119,9 @@ struct PlayerDetailView: View { } } label: { Text("Licence") + CopyPasteButtonView(pasteValue: player.licenceId?.strippedLicense) } - RowButtonView("Copier dans le presse-papier") { - let pasteboard = UIPasteboard.general - pasteboard.string = player.licenceId?.strippedLicense - } - } - - Section { LabeledContent { TextField("Téléphone", text: $phoneNumber) .keyboardType(.namePhonePad) @@ -140,15 +134,9 @@ struct PlayerDetailView: View { } } label: { Text("Téléphone") + CopyPasteButtonView(pasteValue: player.phoneNumber) } - RowButtonView("Copier dans le presse-papier") { - let pasteboard = UIPasteboard.general - pasteboard.string = player.phoneNumber - } - } - - Section { LabeledContent { TextField("Email", text: $email) .keyboardType(.emailAddress) @@ -161,11 +149,7 @@ struct PlayerDetailView: View { } } label: { Text("Email") - } - - RowButtonView("Copier dans le presse-papier") { - let pasteboard = UIPasteboard.general - pasteboard.string = player.email + CopyPasteButtonView(pasteValue: player.email) } } @@ -173,7 +157,7 @@ struct PlayerDetailView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { ShareLink(item: player.pasteData()) { - Label("Partagez", systemImage: "square.and.arrow.up") + Label("Partager", systemImage: "square.and.arrow.up") } } } diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index e913a61..ee049c7 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -73,13 +73,7 @@ struct RoundSettingsView: View { Text("Suite à un changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de votre tableau et valider que tout est ok.") } } - - Section { - ShareLink(item: tournament.rounds().compactMap { $0.pasteData() }.joined(separator: "\n\n")) { - Text("Exportez l'état du tableau") - } - } - + // Section { // RowButtonView("Enabled", role: .destructive) { // let allMatches = tournament._allMatchesIncludingDisabled() @@ -160,6 +154,11 @@ struct RoundSettingsView: View { } } } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + ShareLink(item: tournament.rounds().compactMap { $0.pasteData() }.joined(separator: "\n\n")) + } + } } private func _removeAllSeeds() async { diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 6b56ee4..3480b61 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -10,14 +10,28 @@ import LeStorage struct EditingTeamView: View { @EnvironmentObject var dataStore: DataStore - + @EnvironmentObject var networkMonitor: NetworkMonitor + @Environment(Tournament.self) var tournament: Tournament var team: TeamRegistration + @State private var editedTeam: TeamRegistration? + @State private var contactType: ContactType? = nil + @State private var sentError: ContactManagerError? = nil + @State private var showSubscriptionView: Bool = false @State private var registrationDate : Date @State private var callDate : Date @State private var name: String - @Environment(Tournament.self) var tournament: Tournament + var messageSentFailed: Binding { + Binding { + sentError != nil + } set: { newValue in + if newValue == false { + sentError = nil + } + } + } + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -32,52 +46,111 @@ struct EditingTeamView: View { var body: some View { List { Section { - TextField("Nom de l'équipe", text: $name) - .autocorrectionDisabled() - .keyboardType(.alphabet) - .frame(maxWidth: .infinity) - .submitLabel(.done) - .onSubmit(of: .text) { - let trimmed = name.trimmed - if trimmed.isEmpty { - team.name = nil - } else { - team.name = trimmed - } - - _save() - } + RowButtonView("Modifier la composition de l'équipe") { + editedTeam = team + } + TeamDetailView(team: team) + } footer: { + HStack { + CopyPasteButtonView(pasteValue: team.playersPasteData()) + Spacer() + NavigationLink { + GroupStageTeamReplacementView(team: team) + .environment(tournament) + } label: { + Text("Chercher à remplacer") + } + } } + Section { - DatePicker(registrationDate.localizedWeekDay(), selection: $registrationDate) - } header: { - Text("Date d'inscription") - } - - Section { + DatePicker(selection: $registrationDate) { + Text("Date d'inscription") + Text(registrationDate.localizedWeekDay()) + } if let callDate = team.callDate { LabeledContent() { Text(callDate.localizedDate()) } label: { - Text("OK") + Text("Convocation") } } else { Text("Cette équipe n'a pas été convoquée") } - } header: { - Text("Statut de la convocation") } Section { Toggle(isOn: hasArrived) { Text("Équipe sur place") } - /* - Toggle(isOn: $team.confirmedCall) { - Text("Équipe sur place") + + Toggle(isOn: .init(get: { + return team.wildCardBracket + }, set: { value in + team.resetPositions() + team.wildCardGroupStage = false + team.walkOut = false + team.wildCardBracket = value + do { + try tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + })) { + Text("Wildcard Tableau") + } + + Toggle(isOn: .init(get: { + return team.wildCardGroupStage + }, set: { value in + team.resetPositions() + team.wildCardBracket = false + team.walkOut = false + team.wildCardGroupStage = value + do { + try tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + })) { + Text("Wildcard Poule") + } + + Toggle(isOn: .init(get: { + return team.walkOut + }, set: { value in + team.resetPositions() + team.wildCardBracket = false + team.wildCardGroupStage = false + team.walkOut = value + do { + try tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + })) { + Text("Forfait") } - */ + } + + Section { + TextField("Nom de l'équipe", text: $name) + .autocorrectionDisabled() + .keyboardType(.alphabet) + .frame(maxWidth: .infinity) + .submitLabel(.done) + .onSubmit(of: .text) { + let trimmed = name.trimmed + if trimmed.isEmpty { + team.name = nil + } else { + team.name = trimmed + } + + _save() + } + } Section { @@ -95,15 +168,101 @@ struct EditingTeamView: View { } .disabled(team.inRound() == false) } + + Section { + RowButtonView("Effacer l'équipe", role: .destructive, systemImage: "trash") { + team.deleteTeamScores() + do { + try tournamentStore.teamRegistrations.delete(instance: team) + } catch { + Logger.error(error) + } + } + } + } + .alert("Un problème est survenu", isPresented: messageSentFailed) { + Button("OK") { + } + } message: { + Text(_getErrorMessage()) + } + .sheet(item: $contactType) { contactType in + Group { + switch contactType { + case .message(_, let recipients, let body, _): + if Guard.main.paymentForNewTournament() != nil { + MessageComposeView(recipients: recipients, body: body) { result in + switch result { + case .cancelled: + break + case .failed: + self.sentError = .messageFailed + case .sent: + if networkMonitor.connected == false { + self.sentError = .messageNotSent + } + @unknown default: + break + } + } + } else { + SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true) + .environment(\.colorScheme, .light) + } + case .mail(_, let recipients, let bccRecipients, let body, let subject, _): + if Guard.main.paymentForNewTournament() != nil { + MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in + switch result { + case .cancelled, .saved: + self.contactType = nil + case .failed: + self.contactType = nil + self.sentError = .mailFailed + case .sent: + if networkMonitor.connected == false { + self.contactType = nil + self.sentError = .mailNotSent + } + @unknown default: + break + } + } + } else { + SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true) + .environment(\.colorScheme, .light) + } + } + } + .tint(.master) + } + .sheet(item: $editedTeam) { editedTeam in + NavigationStack { + AddTeamView(tournament: tournament, editedTeam: editedTeam) + } + .tint(.master) + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + MenuWarningView(tournament: tournament, teams: [team], contactType: $contactType) + } } .onChange(of: registrationDate) { team.registrationDate = registrationDate _save() } - .headerProminence(.increased) - .navigationTitle("Statut de l'équipe") - .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Édition de l'équipe") + .navigationBarTitleDisplayMode(.inline) + } + + private func _getErrorMessage() -> String { + let m1 : String? = (networkMonitor.connected == false ? "L'appareil n'est pas connecté à internet." : nil) + let m2 : String? = (sentError == .mailNotSent ? "Le mail est dans la boîte d'envoi de l'app Mail. Vérifiez son état dans l'app Mail avant d'essayer de le renvoyer." : nil) + let m3 : String? = ((sentError == .messageFailed || sentError == .messageNotSent) ? "Le SMS n'a pas été envoyé" : nil) + let m4 : String? = (sentError == .mailFailed ? "Le mail n'a pas été envoyé" : nil) + + let message : String = [m1, m2, m3, m4].compacted().joined(separator: "\n") + return message } private var hasArrived: Binding { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index dce6202..493163c 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -524,7 +524,7 @@ struct InscriptionManagerView: View { ForEach(teams) { team in let teamIndex = team.index(in: sortedTeams) NavigationLink { - _teamCompactTeamEditionView(team) + EditingTeamView(team: team) .environment(tournament) } label: { TeamRowView(team: team) @@ -572,113 +572,6 @@ struct InscriptionManagerView: View { .autocorrectionDisabled() } - func _teamCompactTeamEditionView(_ team: TeamRegistration) -> some View { - List { - Section { - TeamDetailView(team: team) - } footer: { - FooterButtonView("Copier dans le presse-papier") { - let pasteboard = UIPasteboard.general - pasteboard.string = team.playersPasteData() - } - } - - Section { - NavigationLink { - EditingTeamView(team: team) - .environment(tournament) - } label: { - Text("Modifier le statut de l'équipe") - Text("Nom de l'équipe, date d'inscription, présence, position") - } - - NavigationLink { - GroupStageTeamReplacementView(team: team) - .environment(tournament) - } label: { - Text("Chercher à remplacer") - } - - RowButtonView("Modifier la composition de l'équipe") { - editedTeam = team - } - } - - Section { - Toggle(isOn: .init(get: { - return team.wildCardBracket - }, set: { value in - team.resetPositions() - team.wildCardGroupStage = false - team.walkOut = false - team.wildCardBracket = value - do { - try tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) - } - _setHash() - })) { - Text("Wildcard Tableau") - } - - Toggle(isOn: .init(get: { - return team.wildCardGroupStage - }, set: { value in - team.resetPositions() - team.wildCardBracket = false - team.walkOut = false - team.wildCardGroupStage = value - do { - try tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) - } - _setHash() - })) { - Text("Wildcard Poule") - } - } - - Section { - Toggle(isOn: .init(get: { - return team.walkOut - }, set: { value in - team.resetPositions() - team.wildCardBracket = false - team.wildCardGroupStage = false - team.walkOut = value - do { - try tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) - } - _setHash() - })) { - Text("Forfait") - } - - RowButtonView("Effacer l'équipe", role: .destructive, systemImage: "trash") { - team.deleteTeamScores() - do { - try tournamentStore.teamRegistrations.delete(instance: team) - } catch { - Logger.error(error) - } - _setHash() - } - } - } - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - MenuWarningView(tournament: tournament, teams: [team], contactType: $contactType) - } - } - .toolbarBackground(.visible, for: .navigationBar) - .navigationTitle("Édition de l'équipe") - .navigationBarTitleDisplayMode(.inline) - } - @ViewBuilder func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View { Section { @@ -1044,96 +937,12 @@ struct InscriptionManagerView: View { Text(formattedRegistrationDate) } Spacer() - Menu { - _teamMenuOptionView(team) - } label: { - LabelOptions().labelStyle(.titleOnly) - } - } - } - - @ViewBuilder - private func _teamMenuOptionView(_ team: TeamRegistration) -> some View { - Section { - NavigationLink { - GroupStageTeamReplacementView(team: team) - } label: { - Text("Chercher à remplacer") - } - - MenuWarningView(tournament: tournament, teams: [team], contactType: $contactType) - //Divider() - Button("Copier") { - let pasteboard = UIPasteboard.general - pasteboard.string = team.playersPasteData() - } - //Divider() - Button("Changer les joueurs") { - editedTeam = team - } - Divider() NavigationLink { EditingTeamView(team: team) .environment(tournament) } label: { - Text("Modifier une donnée de l'équipe") - } - Divider() - Toggle(isOn: .init(get: { - return team.wildCardBracket - }, set: { value in - team.resetPositions() - team.wildCardGroupStage = false - team.walkOut = false - team.wildCardBracket = value - do { - try tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) - } - _setHash() - })) { - Label("Wildcard Tableau", systemImage: team.wildCardBracket ? "circle.inset.filled" : "circle") - } - - Toggle(isOn: .init(get: { - return team.wildCardGroupStage - }, set: { value in - team.resetPositions() - team.wildCardBracket = false - team.walkOut = false - team.wildCardGroupStage = value - do { - try tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) - } - _setHash() - })) { - Label("Wildcard Poule", systemImage: team.wildCardGroupStage ? "circle.inset.filled" : "circle") - } - - Divider() - Toggle(isOn: .init(get: { - return team.walkOut - }, set: { value in - team.resetPositions() - team.wildCardBracket = false - team.wildCardGroupStage = false - team.walkOut = value - do { - try tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) - } - _setHash() - })) { - Label("WO", systemImage: team.walkOut ? "circle.inset.filled" : "circle") + LabelOptions().labelStyle(.titleOnly) } - Divider() - _teamDeleteButtonView(team) -// } header: { -// Text(team.teamLabel(.short)) } } diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 55db055..6ca1e6c 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -253,9 +253,15 @@ struct TableStructureView: View { private func _saveWithoutRebuild() { tournament.teamCount = teamCount tournament.groupStageCount = groupStageCount + let updateGroupStageState = teamsPerGroupStage > tournament.teamsPerGroupStage tournament.teamsPerGroupStage = teamsPerGroupStage tournament.qualifiedPerGroupStage = qualifiedPerGroupStage tournament.groupStageAdditionalQualified = groupStageAdditionalQualified + if updateGroupStageState { + tournament.groupStages().forEach { groupStage in + groupStage.updateGroupStageState() + } + } do { try dataStore.tournaments.addOrUpdate(instance: tournament) } catch { diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index 03386c3..88b0031 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -20,9 +20,6 @@ struct TournamentRankView: View { var tournamentStore: TournamentStore { return self.tournament.tournamentStore } - - @State private var runningMatches: [Match]? - @State private var matchesLeft: [Match]? var isEditingTeam: Binding { Binding { @@ -37,6 +34,10 @@ struct TournamentRankView: View { let rankingsCalculated = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil }) if editMode?.wrappedValue.isEditing == false { Section { + let all = tournament.allMatches() + let runningMatches = tournament.runningMatches(all) + let matchesLeft = tournament.readyMatches(all) + MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false) MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false) @@ -105,11 +106,6 @@ struct TournamentRankView: View { } } } - .task { - let all = tournament.allMatches() - self.runningMatches = await tournament.asyncRunningMatches(all) - self.matchesLeft = await tournament.readyMatches(all) - } .alert("Position", isPresented: isEditingTeam) { if let selectedTeam { @Bindable var team = selectedTeam