diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 90dd136..07107fa 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -116,7 +116,6 @@ 7A71580C2C44453200852088 /* AdsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A71580B2C44453200852088 /* AdsScreen.swift */; }; 7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A71580D2C4445A200852088 /* AdsCoordinator.swift */; }; 7A7158122C444A6400852088 /* AdsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158112C444A6400852088 /* AdsViewModel.swift */; }; - 7A71EF552D0A208F00943129 /* MarkerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A71EF542D0A208F00943129 /* MarkerModel.swift */; }; 7A71EF572D0A26B200943129 /* EventModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A71EF562D0A26B200943129 /* EventModel.swift */; }; 7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */; }; 7A761C042677F18E0005F28F /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474323FF06CA00B424AF /* ApiService.swift */; }; @@ -394,7 +393,6 @@ 7A71580B2C44453200852088 /* AdsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsScreen.swift; sourceTree = ""; }; 7A71580D2C4445A200852088 /* AdsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsCoordinator.swift; sourceTree = ""; }; 7A7158112C444A6400852088 /* AdsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsViewModel.swift; sourceTree = ""; }; - 7A71EF542D0A208F00943129 /* MarkerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkerModel.swift; sourceTree = ""; }; 7A71EF562D0A26B200943129 /* EventModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventModel.swift; sourceTree = ""; }; 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclePhotoCell.swift; sourceTree = ""; }; 7A761C0A267E8FF90005F28F /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; @@ -994,7 +992,6 @@ 7AB9FE252D08C2D7005DE374 /* EventsCoordinator.swift */, 7AB9FE272D08C2F4005DE374 /* EventsViewModel.swift */, 7AB9FE292D08CF35005DE374 /* EventsScreenMode.swift */, - 7A71EF542D0A208F00943129 /* MarkerModel.swift */, 7A71EF562D0A26B200943129 /* EventModel.swift */, ); path = EventsScreen; @@ -1422,7 +1419,6 @@ 7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */, 7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */, 7A71580C2C44453200852088 /* AdsScreen.swift in Sources */, - 7A71EF552D0A208F00943129 /* MarkerModel.swift in Sources */, 7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */, 7A91894F29A2BD8700519C74 /* GestureRecognizers.swift in Sources */, 7AFBE8CC2C3085C6003C491D /* ACProgressView.swift in Sources */, diff --git a/AutoCat/Screens/EventsScreen/EventModel.swift b/AutoCat/Screens/EventsScreen/EventModel.swift index ab31681..1ee9576 100644 --- a/AutoCat/Screens/EventsScreen/EventModel.swift +++ b/AutoCat/Screens/EventsScreen/EventModel.swift @@ -6,12 +6,13 @@ // Copyright © 2024 Selim Mustafaev. All rights reserved. // -import Foundation +import CoreLocation struct EventModel: Identifiable { - let id = UUID() + let id: String + var date: String + var coordinate: CLLocationCoordinate2D var address: String - var marker: MarkerModel var isMe: Bool } diff --git a/AutoCat/Screens/EventsScreen/EventsCoordinator.swift b/AutoCat/Screens/EventsScreen/EventsCoordinator.swift index 6917afe..65c6b37 100644 --- a/AutoCat/Screens/EventsScreen/EventsCoordinator.swift +++ b/AutoCat/Screens/EventsScreen/EventsCoordinator.swift @@ -34,4 +34,10 @@ class EventsCoordinator { let coordinator = LocationEditCoordinator(navController: navController) return await coordinator.start() } + + func editEvent(event: VehicleEventDto) async -> VehicleEventDto? { + + let coordinator = LocationEditCoordinator(navController: navController, event: event) + return await coordinator.start() + } } diff --git a/AutoCat/Screens/EventsScreen/EventsScreen.swift b/AutoCat/Screens/EventsScreen/EventsScreen.swift index 8c6d763..04554de 100644 --- a/AutoCat/Screens/EventsScreen/EventsScreen.swift +++ b/AutoCat/Screens/EventsScreen/EventsScreen.swift @@ -43,7 +43,7 @@ struct EventsScreen: View { var map: some View { Map { - ForEach(viewModel.events.map(\.marker)) { + ForEach(viewModel.events) { Marker($0.date, coordinate: $0.coordinate) } } @@ -53,6 +53,12 @@ struct EventsScreen: View { List { ForEach(viewModel.events) { event in makeEventCell(for: event) + .swipeActions(allowsFullSwipe: false) { + makeActions(for: event) + } + .contextMenu { + makeActions(for: event, useLabels: true) + } } } .listStyle(.inset) @@ -62,7 +68,7 @@ struct EventsScreen: View { HStack { VStack(alignment: .leading, spacing: 4) { Text(event.address) - Text(event.marker.date) + Text(event.date) .foregroundStyle(.secondary) } Spacer() @@ -70,6 +76,27 @@ struct EventsScreen: View { .foregroundStyle(event.isMe ? Color.accentColor : .secondary) } } + + @ViewBuilder + func makeActions(for event: EventModel, useLabels: Bool = false) -> some View { + Button(role: .destructive) { + Task { await viewModel.deleteEvent(event) } + } label: { + Label(useLabels ? "Delete" : "", systemImage: "trash") + } + + Button() { + Task { await viewModel.editEvent(event) } + } label: { + Label(useLabels ? "Edit" : "", systemImage: "pencil") + } + + Button() { + + } label: { + Label(useLabels ? "Copy" : "", systemImage: "doc.on.doc") + } + } } #Preview { diff --git a/AutoCat/Screens/EventsScreen/EventsViewModel.swift b/AutoCat/Screens/EventsScreen/EventsViewModel.swift index 7671f8b..cf5300c 100644 --- a/AutoCat/Screens/EventsScreen/EventsViewModel.swift +++ b/AutoCat/Screens/EventsScreen/EventsViewModel.swift @@ -36,24 +36,22 @@ class EventsViewModel: ACHudContainer { func updateEvents() { - events = vehicle.events.map { event in + events = vehicle.events.sorted { $0.date > $1.date }.map { event in - EventModel( + let date = Date(timeIntervalSince1970: event.date) + let dateString = Formatters.marker.string(from: date) + let coordinate = CLLocationCoordinate2DMake(event.latitude, event.longitude) + + return EventModel( + id: event.id, + date: dateString, + coordinate: coordinate, address: event.address ?? "Lat: \(event.latitude), Lon: \(event.longitude)", - marker: makeMarkerModel(from: event), isMe: event.addedBy == settingsService.user.email ) } } - func makeMarkerModel(from event: VehicleEventDto) -> MarkerModel { - - let date = Date(timeIntervalSince1970: event.date) - let dateString = Formatters.marker.string(from: date) - let coordinate = CLLocationCoordinate2DMake(event.latitude, event.longitude) - return MarkerModel(date: dateString, coordinate: coordinate) - } - func addNewEvent() async { if let event = await coordinator?.addNewEvent() { await eventOperation { @@ -61,6 +59,34 @@ class EventsViewModel: ACHudContainer { } apiOperation: { try await self.apiService.add(event: event, to: self.vehicle.getNumber()) } + + updateEvents() + } + } + + func deleteEvent(_ event: EventModel) async { + + await eventOperation { + try await self.storageService.deleteNote(id: event.id, for: self.vehicle.getNumber()) + } apiOperation: { + try await self.apiService.remove(event: event.id) + } + + updateEvents() + } + + func editEvent(_ event: EventModel) async { + guard let eventDto = vehicle.events.first(where: { $0.id == event.id }) else { + return + } + + if let updatedEvent = await coordinator?.editEvent(event: eventDto) { + await eventOperation { + try await self.storageService.edit(event: updatedEvent, for: self.vehicle.getNumber()) + } apiOperation: { + try await self.apiService.edit(event: updatedEvent) + } + updateEvents() } } diff --git a/AutoCat/Screens/EventsScreen/MarkerModel.swift b/AutoCat/Screens/EventsScreen/MarkerModel.swift deleted file mode 100644 index 76659d1..0000000 --- a/AutoCat/Screens/EventsScreen/MarkerModel.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// MarkerModel.swift -// AutoCat -// -// Created by Selim Mustafaev on 11.12.2024. -// Copyright © 2024 Selim Mustafaev. All rights reserved. -// - -import CoreLocation - -struct MarkerModel: Identifiable { - - let id = UUID() - var date: String - var coordinate: CLLocationCoordinate2D -} diff --git a/AutoCat/Screens/NotesScreen/NotesScreen.swift b/AutoCat/Screens/NotesScreen/NotesScreen.swift index 70033d3..0726b32 100644 --- a/AutoCat/Screens/NotesScreen/NotesScreen.swift +++ b/AutoCat/Screens/NotesScreen/NotesScreen.swift @@ -63,12 +63,11 @@ struct NotesScreen: View { @ViewBuilder func makeActions(for note: VehicleNoteDto, useLabels: Bool = false) -> some View { - Button() { + Button(role: .destructive) { Task { await viewModel.deleteNote(id: note.id) } } label: { Label(useLabels ? "Delete" : "", systemImage: "trash") } - .tint(.red) Button() { selectedNoteId = note.id diff --git a/AutoCatCore/Services/StorageService/StorageService+Events.swift b/AutoCatCore/Services/StorageService/StorageService+Events.swift index 6513e35..6e01ee6 100644 --- a/AutoCatCore/Services/StorageService/StorageService+Events.swift +++ b/AutoCatCore/Services/StorageService/StorageService+Events.swift @@ -49,8 +49,9 @@ public extension StorageService { } try await realm.asyncWrite { - if let index = vehicle.events.firstIndex(where: { $0.id == event.id }) { - vehicle.events[index] = VehicleEvent(dto: event) + if vehicle.events.contains(where: { $0.id == event.id }) { + realm.add(VehicleEvent(dto: event), update: .modified) + vehicle.updatedDate = Date().timeIntervalSince1970 } else { throw StorageError.eventNotFound } diff --git a/AutoCatCoreTests/Storage/StorageServiceTests+Events.swift b/AutoCatCoreTests/Storage/StorageServiceTests+Events.swift index 868c5df..5ee0e9d 100644 --- a/AutoCatCoreTests/Storage/StorageServiceTests+Events.swift +++ b/AutoCatCoreTests/Storage/StorageServiceTests+Events.swift @@ -68,14 +68,33 @@ extension StorageServiceTests { let event = VehicleEventDto(lat: 0, lon: 0) var vehicle = try await storageService.add(event: event, to: existingVehicleNumber) - let id = try #require(vehicle.events.first?.id) - let editedEvent = VehicleEventDto(lat: testLat, lon: testLon) + var editedEvent = VehicleEventDto(lat: testLat, lon: testLon) + editedEvent.id = try #require(vehicle.events.first?.id) + vehicle = try await storageService.edit(event: editedEvent, for: existingVehicleNumber) - let resultEcent = try #require(vehicle.events.first { $0.id == id }!) - #expect(resultEcent.latitude == testLat && resultEcent.longitude == testLon) + let resultEvent = try #require(vehicle.events.first { $0.id == editedEvent.id }) + #expect(resultEvent.latitude == testLat && resultEvent.longitude == testLon) #expect(vehicle.events.count == 1) #expect(vehicle.updatedDate != 0) } + + @Test("Edit non-existent event") + func editNonExistentEvent() async throws { + + let event = VehicleEventDto(lat: 0, lon: 0) + await #expect(throws: StorageError.eventNotFound) { + _ = try await storageService.edit(event: event, for: existingVehicleNumber) + } + } + + @Test("Edit event in non-existent vehicle") + func editEventInNonExistentVehicle() async throws { + + let event = VehicleEventDto(lat: 0, lon: 0) + await #expect(throws: StorageError.vehicleNotFound) { + _ = try await storageService.edit(event: event, for: nonExistingVehicleNumber) + } + } } diff --git a/AutoCatCoreTests/Storage/StorageServiceTests+Notes.swift b/AutoCatCoreTests/Storage/StorageServiceTests+Notes.swift new file mode 100644 index 0000000..d439cdb --- /dev/null +++ b/AutoCatCoreTests/Storage/StorageServiceTests+Notes.swift @@ -0,0 +1,89 @@ +// +// StorageServiceTests+Notes.swift +// AutoCatCoreTests +// +// Created by Selim Mustafaev on 13.12.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Testing +import AutoCatCore + +extension StorageServiceTests { + + @Test("Add note to vehicle") + func addNote() async throws { + + let vehicle = try await storageService.addNote(text: noteText, to: existingVehicleNumber) + + #expect(vehicle.number == existingVehicleNumber) + #expect(vehicle.notes.contains { $0.text == noteText }) + } + + @Test("Adding note to non-existent vehicle") + func addNoteError() async throws { + + await #expect(throws: StorageError.vehicleNotFound) { + _ = try await storageService.addNote(text: noteText, to: nonExistingVehicleNumber) + } + } + + @Test("Edit note") + func editNote() async throws { + + let newNoteText = "New test text" + + var vehicle = try await storageService.addNote(text: noteText, to: existingVehicleNumber) + let note = try #require(vehicle.notes.first { $0.text == noteText }) + + vehicle = try await storageService.editNote(id: note.id, text: newNoteText, for: existingVehicleNumber) + + #expect(vehicle.number == existingVehicleNumber) + #expect(vehicle.notes.contains { $0.text == newNoteText }) + #expect(!vehicle.notes.contains { $0.text == noteText }) + } + + @Test("Edit note of non-existent vehicle") + func addNoteNonExistentVehicle() async throws { + + await #expect(throws: StorageError.vehicleNotFound) { + _ = try await storageService.editNote(id: "", text: "", for: nonExistingVehicleNumber) + } + } + + @Test("Edit non-existent note") + func editNonExistentNote() async throws { + + await #expect(throws: StorageError.noteNotFound) { + _ = try await storageService.editNote(id: "", text: "", for: existingVehicleNumber) + } + } + + @Test("Delete note") + func deleteNote() async throws { + + var vehicle = try await storageService.addNote(text: noteText, to: existingVehicleNumber) + let note = try #require(vehicle.notes.first { $0.text == noteText }) + + vehicle = try await storageService.deleteNote(id: note.id, for: existingVehicleNumber) + + #expect(vehicle.number == existingVehicleNumber) + #expect(!vehicle.notes.contains { $0.text == noteText }) + } + + @Test("Delete note from non-existent vehicle") + func deleteNoteNonExistentVehicle() async throws { + + await #expect(throws: StorageError.vehicleNotFound) { + _ = try await storageService.deleteNote(id: "", for: nonExistingVehicleNumber) + } + } + + @Test("Delete non-existent note") + func deleteNonExistentNote() async throws { + + await #expect(throws: StorageError.noteNotFound) { + _ = try await storageService.deleteNote(id: "", for: existingVehicleNumber) + } + } +} diff --git a/AutoCatCoreTests/Storage/StorageServiceTests.swift b/AutoCatCoreTests/Storage/StorageServiceTests.swift index eeb5705..addc3f5 100644 --- a/AutoCatCoreTests/Storage/StorageServiceTests.swift +++ b/AutoCatCoreTests/Storage/StorageServiceTests.swift @@ -34,7 +34,7 @@ struct StorageServiceTests { func addTestVehicle(config: Realm.Configuration) throws { - var vehicle = Vehicle(existingVehicleNumber) + let vehicle = Vehicle(existingVehicleNumber) vehicle.addedDate = 0 vehicle.updatedDate = 0 @@ -44,82 +44,6 @@ struct StorageServiceTests { } } - @Test("Add note to vehicle") - func addNote() async throws { - - let vehicle = try await storageService.addNote(text: noteText, to: existingVehicleNumber) - - #expect(vehicle.number == existingVehicleNumber) - #expect(vehicle.notes.contains { $0.text == noteText }) - } - - @Test("Adding note to non-existent vehicle") - func addNoteError() async throws { - - await #expect(throws: StorageError.vehicleNotFound) { - _ = try await storageService.addNote(text: noteText, to: nonExistingVehicleNumber) - } - } - - @Test("Edit note") - func editNote() async throws { - - let newNoteText = "New test text" - - var vehicle = try await storageService.addNote(text: noteText, to: existingVehicleNumber) - let note = try #require(vehicle.notes.first { $0.text == noteText }) - - vehicle = try await storageService.editNote(id: note.id, text: newNoteText, for: existingVehicleNumber) - - #expect(vehicle.number == existingVehicleNumber) - #expect(vehicle.notes.contains { $0.text == newNoteText }) - #expect(!vehicle.notes.contains { $0.text == noteText }) - } - - @Test("Edit note of non-existent vehicle") - func addNoteNonExistentVehicle() async throws { - - await #expect(throws: StorageError.vehicleNotFound) { - _ = try await storageService.editNote(id: "", text: "", for: nonExistingVehicleNumber) - } - } - - @Test("Edit non-existent note") - func editNonExistentNote() async throws { - - await #expect(throws: StorageError.noteNotFound) { - _ = try await storageService.editNote(id: "", text: "", for: existingVehicleNumber) - } - } - - @Test("Delete note") - func deleteNote() async throws { - - var vehicle = try await storageService.addNote(text: noteText, to: existingVehicleNumber) - let note = try #require(vehicle.notes.first { $0.text == noteText }) - - vehicle = try await storageService.deleteNote(id: note.id, for: existingVehicleNumber) - - #expect(vehicle.number == existingVehicleNumber) - #expect(!vehicle.notes.contains { $0.text == noteText }) - } - - @Test("Delete note from non-existent vehicle") - func deleteNoteNonExistentVehicle() async throws { - - await #expect(throws: StorageError.vehicleNotFound) { - _ = try await storageService.deleteNote(id: "", for: nonExistingVehicleNumber) - } - } - - @Test("Delete non-existent note") - func deleteNonExistentNote() async throws { - - await #expect(throws: StorageError.noteNotFound) { - _ = try await storageService.deleteNote(id: "", for: existingVehicleNumber) - } - } - @Test("Load existing vehicle") func loadExistingVehicle() async throws {