From ff0b236afc9fc64315240d258d92ccaaac53e06f Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 4 Apr 2024 07:04:36 +0200 Subject: [PATCH] add court management --- PadelClub.xcodeproj/project.pbxproj | 4 ++ PadelClub/Data/Match.swift | 51 +++++++++++++++---- PadelClub/Data/Tournament.swift | 5 ++ PadelClub/PadelClubApp.swift | 4 +- PadelClub/ViewModel/NavigationViewModel.swift | 14 +++++ PadelClub/Views/Match/MatchDetailView.swift | 26 +++++----- PadelClub/Views/Match/MatchSummaryView.swift | 2 +- .../Navigation/Agenda/ActivityView.swift | 29 ++++++----- .../Shared/TournamentCellView.swift | 17 ++++--- 9 files changed, 107 insertions(+), 45 deletions(-) create mode 100644 PadelClub/ViewModel/NavigationViewModel.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 47c1c1d..f1165bc 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -173,6 +173,7 @@ FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; }; FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */; }; FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065D2BBD8040009D6715 /* MatchListView.swift */; }; + FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */; }; FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; }; FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; @@ -416,6 +417,7 @@ FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = ""; }; FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageTeamView.swift; sourceTree = ""; }; FFBF065D2BBD8040009D6715 /* MatchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchListView.swift; sourceTree = ""; }; + FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = ""; }; FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; @@ -812,6 +814,7 @@ FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */, FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */, FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, + FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -1275,6 +1278,7 @@ FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, + FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 43626b8..ef1623b 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -171,22 +171,49 @@ class Match: ModelObject, Storable { func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup) { if hasEnded() == false { startDate = fromStartDate -// if match.isTournamentMatch() { -// switch fieldSetup { -// case .random: -// let field = match.freeFields.randomElement() ?? match.currentTournament?.freeFields.randomElement() ?? 1 -// match.setupFieldAndStartDateIfPossible(field) -// case .field(let courtIndex): -// let fieldIndex = Int64(courtIndex) -// match.setupFieldAndStartDateIfPossible(fieldIndex) -// } -// } + + switch fieldSetup { + case .random: + let courtName = availableCourts().randomElement() + court = courtName + case .field(let courtName): + court = courtName + } + } else { startDate = fromStartDate endDate = toEndDate } } + func courtCount() -> Int { + currentTournament()?.courtCount ?? 1 + } + + func courtIsAvailable(_ courtIndex: Int) -> Bool { + let courtUsed = currentTournament()?.courtUsed() ?? [] + return courtUsed.contains(String(courtIndex)) == false +// return Set(availableCourts().map { String($0) }).subtracting(Set(courtUsed)) + } + + func courtIsPreferred(_ courtIndex: Int) -> Bool { + false + } + + func availableCourts() -> [String] { + let courtUsed = currentTournament()?.courtUsed() ?? [] + let availableCourts = Array(1...courtCount()) + return Array(Set(availableCourts.map { String($0) }).subtracting(Set(courtUsed))) + } + + func removeCourt() { + court = nil + } + + func setCourt(_ courtIndex: Int) { + court = String(courtIndex) + } + func canBeStarted() -> Bool { let teams = teams() guard teams.count == 2 else { return false } @@ -241,6 +268,10 @@ class Match: ModelObject, Storable { groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject() } + func tournamentId() -> String? { + groupStageObject?.tournament ?? roundObject?.tournament + } + func scores() -> [TeamScore] { Store.main.filter(isIncluded: { $0.match == id }) } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 42b8569..df68985 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -86,6 +86,11 @@ class Tournament : ModelObject, Storable { case build } + func courtUsed() -> [String] { + let runningMatches : [Match] = Store.main.filter(isIncluded: { $0.isRunning() }).filter({ $0.tournamentId() == self.id }) + return Set(runningMatches.compactMap { $0.court }).sorted() + } + func hasStarted() -> Bool { startDate <= Date() } diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 6d3e7a9..bc1f33d 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -12,10 +12,12 @@ import TipKit @main struct PadelClubApp: App { let persistenceController = PersistenceController.shared - + @State private var navigationViewModel = NavigationViewModel() + var body: some Scene { WindowGroup { MainView() + .environment(navigationViewModel) .accentColor(.launchScreenBackground) .onAppear { self._onAppear() diff --git a/PadelClub/ViewModel/NavigationViewModel.swift b/PadelClub/ViewModel/NavigationViewModel.swift new file mode 100644 index 0000000..b39d960 --- /dev/null +++ b/PadelClub/ViewModel/NavigationViewModel.swift @@ -0,0 +1,14 @@ +// +// NavigationViewModel.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/04/2024. +// + +import SwiftUI + +@Observable +class NavigationViewModel { + var agendaDestination: AgendaDestination? = .activity + var tournament: Tournament? +} diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index ccf3b9b..cfe0778 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -103,15 +103,15 @@ struct MatchDetailView: View { if match.hasEnded() == false { Menu { Button("Non défini") { - match.court = nil + match.removeCourt() save() } -// ForEach(1...match.numberOfField, id: \.self) { courtIndex in -// Button("Terrain #\(courtIndex.formatted())") { -// match.fieldIndex = Int64(courtIndex) -// save() -// } -// } + ForEach(1...match.courtCount(), id: \.self) { courtIndex in + Button("Terrain #\(courtIndex.formatted())") { + match.setCourt(courtIndex) + save() + } + } } label: { VStack(alignment: .leading) { Text("terrain").font(.footnote).foregroundStyle(.secondary) @@ -377,9 +377,10 @@ struct MatchDetailView: View { if match.isReady() { Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) - Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration)) Text("Tout de suite").tag(MatchDateSetup.now) } + Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-match.matchFormat.estimatedDuration)) + Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration)) Text("À").tag(MatchDateSetup.customDate) } label: { Text("Horaire") @@ -414,11 +415,10 @@ struct MatchDetailView: View { Picker(selection: $fieldSetup) { Text("Au hasard").tag(MatchFieldSetup.random) //Text("Premier disponible").tag(MatchFieldSetup.firstAvailable) -// ForEach(1...match.numberOfField, id: \.self) { courtIndex in -// let fieldIndex = Int64(courtIndex) -// let fieldIsAvailable : Bool = match.currentTournament?.fieldIsAvailable(fieldIndex) ?? true -// Label("Terrain #\(courtIndex)", systemImage: match.isFieldPreferred(fieldIndex) ? "heart" : "").tag(MatchFieldSetup.field(courtIndex)) -// } + ForEach(1...match.courtCount(), id: \.self) { courtIndex in + Text("Terrain #\(courtIndex)") + .tag(MatchFieldSetup.field(String(courtIndex))) + } } label: { Text("Choix du terrain") } diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 7a1f021..e312219 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -74,7 +74,7 @@ struct MatchSummaryView: View { Spacer() if let court = match.court, match.hasEnded() == false { Spacer() - Text("Terrain \(court)") + Text("Terrain #\(court)") } } } diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index a6271f0..9655ce0 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -9,9 +9,9 @@ import SwiftUI struct ActivityView: View { @EnvironmentObject var dataStore: DataStore - + @Environment(NavigationViewModel.self) private var navigation + @State private var searchText: String = "" - @State private var agendaDestination: AgendaDestination? = .activity @State private var filterEnabled: Bool = false @State private var presentToolbar: Bool = false @@ -41,7 +41,7 @@ struct ActivityView: View { } var tournaments: [FederalTournamentHolder] { - switch agendaDestination! { + switch navigation.agendaDestination! { case .activity: runningTournaments case .history: @@ -53,10 +53,11 @@ struct ActivityView: View { var body: some View { NavigationStack { + @Bindable var navigation = navigation VStack(spacing: 0) { - GenericDestinationPickerView(selectedDestination: $agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false) + GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false) List { - switch agendaDestination! { + switch navigation.agendaDestination! { case .activity: EventListView(tournaments: runningTournaments, viewStyle: viewStyle) case .history: @@ -66,7 +67,7 @@ struct ActivityView: View { } } .overlay { - if let error, agendaDestination == .tenup { + if let error, navigation.agendaDestination == .tenup { ContentUnavailableView { Label("Erreur", systemImage: "exclamationmark") } description: { @@ -105,20 +106,20 @@ struct ActivityView: View { EventCreationView(tournaments: [tournament]) } .refreshable { - if agendaDestination == .tenup { + if navigation.agendaDestination == .tenup { federalTournaments.removeAll() _gatherFederalTournaments() } } .task { - if agendaDestination == .tenup + if navigation.agendaDestination == .tenup && dataStore.clubs.isEmpty == false && federalTournaments.isEmpty { _gatherFederalTournaments() } } - .onChange(of: agendaDestination) { - if agendaDestination == .tenup + .onChange(of: navigation.agendaDestination) { + if navigation.agendaDestination == .tenup && dataStore.clubs.isEmpty == false && federalTournaments.isEmpty { _gatherFederalTournaments() @@ -186,6 +187,10 @@ struct ActivityView: View { TournamentView() .environment(tournament) } + .navigationDestination(item: $navigation.tournament) { tournament in + TournamentView() + .environment(tournament) + } } } } @@ -206,7 +211,7 @@ struct ActivityView: View { @ViewBuilder private func _dataEmptyView() -> some View { - switch agendaDestination! { + switch navigation.agendaDestination! { case .activity: _runningEmptyView() case .history: @@ -234,7 +239,7 @@ struct ActivityView: View { newTournament = Tournament.newEmptyInstance() } RowButtonView("Importer via Tenup") { - agendaDestination = .tenup + navigation.agendaDestination = .tenup } } } diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index cf6c708..b796d6b 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -9,6 +9,7 @@ import SwiftUI struct TournamentCellView: View { @EnvironmentObject var dataStore: DataStore + @Environment(NavigationViewModel.self) private var navigation let tournament: FederalTournamentHolder let color: Color = .black @@ -21,11 +22,11 @@ struct TournamentCellView: View { var body: some View { ForEach(tournament.tournaments, id: \.id) { build in - _buildView(build, alreadyExist: event?.existingBuild(build) != nil) + _buildView(build, existingTournament: event?.existingBuild(build)) } } - private func _buildView(_ build: any TournamentBuildHolder, alreadyExist: Bool) -> some View { + private func _buildView(_ build: any TournamentBuildHolder, existingTournament: Tournament?) -> some View { HStack { DateBoxView(date: tournament.startDate, displayStyle: displayStyle) Rectangle() @@ -53,7 +54,10 @@ struct TournamentCellView: View { Text(tournament.sortedTeams().count.formatted()) } else if let federalTournament = tournament as? FederalTournament { Button { - if alreadyExist == false { + if let existingTournament { + navigation.agendaDestination = .activity + navigation.tournament = existingTournament + } else { let event = federalTournament.getEvent() let newTournament = Tournament.newEmptyInstance() newTournament.event = event.id @@ -63,17 +67,14 @@ struct TournamentCellView: View { newTournament.dayDuration = federalTournament.dayDuration newTournament.startDate = federalTournament.startDate try? dataStore.tournaments.addOrUpdate(instance: newTournament) - } else { - //event?.existingBuild(build) } } label: { - Image(systemName: alreadyExist ? "checkmark.circle.fill" : "square.and.arrow.down") + Image(systemName: existingTournament != nil ? "checkmark.circle.fill" : "square.and.arrow.down") .resizable() .scaledToFit() .frame(height: 28) - .tint(alreadyExist ? Color.green : nil) + .tint(existingTournament != nil ? Color.green : nil) } - .buttonStyle(.borderless) } } .font(displayStyle == .wide ? .title : .title3)