diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 921ddae..2d7ba9a 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -106,6 +106,7 @@ 7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE712469DFB600ABE48E /* MediaBrowserViewController.swift */; }; 7A64AE762469DFB600ABE48E /* ContentTransformers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE722469DFB600ABE48E /* ContentTransformers.swift */; }; 7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A659B5A24A3768A0043A0F2 /* Substrings.swift */; }; + 7A6B65B32CFB0DB500AABA6B /* NullifyDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */; }; 7A6C4D9E2C56BCA600982597 /* SwiftLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 7A6C4D9D2C56BCA600982597 /* SwiftLocation */; }; 7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD902242BF4A5009DE740 /* PlateView.swift */; }; 7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90724329144009DE740 /* CenterTextLayer.swift */; }; @@ -148,6 +149,7 @@ 7AAAFADC2C4D1E130050410D /* ACImageSliderView+Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAAFADB2C4D1E130050410D /* ACImageSliderView+Modifier.swift */; }; 7AAAFADE2C4D23620050410D /* ACImageSliderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAAFADD2C4D23620050410D /* ACImageSliderModel.swift */; }; 7AABB1F2267E9CC800D7AB32 /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABB1F1267E9CC800D7AB32 /* SwiftDate */; }; + 7AABBE3B2CF9F85600346588 /* Binding+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABBE3A2CF9F85600346588 /* Binding+Map.swift */; }; 7AABDE26253350C30041AFC6 /* RxSectionedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */; }; 7AB0EF812C5CC0FE00291EE6 /* SwiftLocationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */; }; 7AB0EF892C5D307600291EE6 /* LocationServiceStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB0EF882C5D307600291EE6 /* LocationServiceStub.swift */; }; @@ -380,6 +382,7 @@ 7A64AE722469DFB600ABE48E /* ContentTransformers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentTransformers.swift; sourceTree = ""; }; 7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecord.swift; sourceTree = ""; }; 7A659B5A24A3768A0043A0F2 /* Substrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Substrings.swift; sourceTree = ""; }; + 7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NullifyDate.swift; sourceTree = ""; }; 7A6DD902242BF4A5009DE740 /* PlateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlateView.swift; sourceTree = ""; }; 7A6DD90724329144009DE740 /* CenterTextLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenterTextLayer.swift; sourceTree = ""; }; 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers2.0.otf; sourceTree = ""; }; @@ -418,6 +421,7 @@ 7AAAFAD92C4D1AFE0050410D /* Zoomable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zoomable.swift; sourceTree = ""; }; 7AAAFADB2C4D1E130050410D /* ACImageSliderView+Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ACImageSliderView+Modifier.swift"; sourceTree = ""; }; 7AAAFADD2C4D23620050410D /* ACImageSliderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACImageSliderModel.swift; sourceTree = ""; }; + 7AABBE3A2CF9F85600346588 /* Binding+Map.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Map.swift"; sourceTree = ""; }; 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxSectionedDataSource.swift; sourceTree = ""; }; 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = ""; }; 7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocationProtocol.swift; sourceTree = ""; }; @@ -1058,6 +1062,7 @@ 7A27ADF824A09CAD0035F39E /* CocoaError.swift */, 7AE8424D26109F78002F6B31 /* Exportable.swift */, 7A1CF81529A42117007962DA /* Realm.swift */, + 7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */, ); path = Extensions; sourceTree = ""; @@ -1075,6 +1080,7 @@ 7AF8606F2CBAA24500954D2F /* NavigationLink.swift */, 7A2E11282CCE395300E5CA17 /* OptionalDatePicker.swift */, 7A4927D42CCE438600851C01 /* OptionalBinding.swift */, + 7AABBE3A2CF9F85600346588 /* Binding+Map.swift */, ); path = SwiftUI; sourceTree = ""; @@ -1354,6 +1360,7 @@ 7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */, 7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */, 7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */, + 7AABBE3B2CF9F85600346588 /* Binding+Map.swift in Sources */, 7A27ADF7249FEF690035F39E /* Recorder.swift in Sources */, 7A1E78F62CE900330004B740 /* ReportScreen.swift in Sources */, 7A10226C2C551EC500B84627 /* LocationEditScreen.swift in Sources */, @@ -1452,6 +1459,7 @@ 7A64A2182C19E64800284124 /* VehicleOwnershipPeriodDto.swift in Sources */, 7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */, 7A64A2162C19E4CF00284124 /* VehiclePhotoDto.swift in Sources */, + 7A6B65B32CFB0DB500AABA6B /* NullifyDate.swift in Sources */, 7A7097C22C9EC139007CFDCA /* ServiceContainer.swift in Sources */, 7A7097C62C9EC77A007CFDCA /* ServicePropertyWrapper.swift in Sources */, 7A5D84BE2C1AE44700C2209B /* VehiclePhoto.swift in Sources */, @@ -1697,7 +1705,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 141; + CURRENT_PROJECT_VERSION = 142; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = AutoCat; @@ -1724,7 +1732,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 141; + CURRENT_PROJECT_VERSION = 142; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = AutoCat; diff --git a/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift b/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift index 7d7c2fa..1be2383 100644 --- a/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift +++ b/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift @@ -61,4 +61,12 @@ class FiltersViewModel { func applyFilters() { filterResult = filter } + + func nullifyTime(of date: Date?) -> Date? { + guard let date else { + return nil + } + + return Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: date) + } } diff --git a/AutoCat/Screens/ReportScreen/ReportCoordinator.swift b/AutoCat/Screens/ReportScreen/ReportCoordinator.swift index 616cbb6..a282e24 100644 --- a/AutoCat/Screens/ReportScreen/ReportCoordinator.swift +++ b/AutoCat/Screens/ReportScreen/ReportCoordinator.swift @@ -30,6 +30,7 @@ class ReportCoordinator: Coordinator { navController = viewController?.viewControllers.last as? UINavigationController } else { let viewModel = ReportViewModel(vehicle: vehicle) + viewModel.coordinator = self let controller = UIHostingController(rootView: ReportScreen(viewModel: viewModel)) navController = UINavigationController(rootViewController: controller) } @@ -41,4 +42,36 @@ class ReportCoordinator: Coordinator { viewController?.showDetailViewController(navController, sender: self) } } + + func openEvents(vehicle: VehicleDto) { + + let sb = UIStoryboard(name: "Main", bundle: nil) + let controller = sb.instantiateViewController(identifier: "EventsController") as EventsController + controller.vehicle = self.vehicle + controller.vehicleUpdated = { vehicle in + // TODO: Propagate vehicle update upwards + //self.vehicle = vehicle + } + navController?.pushViewController(controller, animated: true) + } + + func openOsago(contracts: [OsagoDto]) { + guard let navController else { return } + + Task { + let coordinator = OsagoCoordinator(navController: navController, contracts: contracts) + try? await coordinator.start() + } + } + + func openOwners(ownerships: [VehicleOwnershipPeriodDto]) { + guard let navController else { + return + } + + Task { + let coordiantor = OwnersCoordinator(navController: navController, ownerships: ownerships) + try? await coordiantor.start() + } + } } diff --git a/AutoCat/Screens/ReportScreen/ReportScreen.swift b/AutoCat/Screens/ReportScreen/ReportScreen.swift index 4f6b804..94bace6 100644 --- a/AutoCat/Screens/ReportScreen/ReportScreen.swift +++ b/AutoCat/Screens/ReportScreen/ReportScreen.swift @@ -32,10 +32,32 @@ struct ReportScreen: View { LabeledContent("Year", value: String(viewModel.vehicle.year)) LabeledContent("Color", value: viewModel.vehicle.color ?? "") LabeledContent("Category", value: viewModel.vehicle.category ?? "") - LabeledContent("Steering wheel position", - value: viewModel.vehicle.isRightWheel == true ? "Right" : "Left") - LabeledContent("Japanese", - value: viewModel.vehicle.isJapanese == true ? "Yes" : "No") + LabeledContent("Steering wheel position", value: viewModel.steerignWheelPosition) + LabeledContent("Japanese", value: viewModel.isJapanese) + } + + Section("Identifiers") { + LabeledContent("Plate number", value: viewModel.plateNumber) + LabeledContent("VIN", value: viewModel.vehicle.vin1 ?? "") + LabeledContent("STS", value: viewModel.vehicle.sts ?? "") + LabeledContent("PTS", value: viewModel.vehicle.pts ?? "") + } + + Section("Engine") { + LabeledContent("Number", value: viewModel.vehicle.engine?.number ?? "") + LabeledContent("Fuel type", value: viewModel.vehicle.engine?.fuelType ?? "") + LabeledContent("Volume (cm³)", value: String(viewModel.vehicle.engine?.volume ?? 0)) + LabeledContent("Power (HP)", value: String(viewModel.vehicle.engine?.powerHp ?? 0)) + LabeledContent("Power (kw)", value: String(viewModel.vehicle.engine?.powerKw ?? 0)) + } + + Section("History") { + LabeledContent("Events", value: String(viewModel.vehicle.events.count)) + .navigationLink(onTap: viewModel.openEvents) + LabeledContent("OSAGO", value: String(viewModel.vehicle.osagoContracts.count)) + .navigationLink(onTap: viewModel.openOsago) + LabeledContent("Owners", value: String(viewModel.vehicle.ownershipPeriods.count)) + .navigationLink(onTap: viewModel.openOwners) } } } diff --git a/AutoCat/Screens/ReportScreen/ReportViewModel.swift b/AutoCat/Screens/ReportScreen/ReportViewModel.swift index 6993c80..038ca4f 100644 --- a/AutoCat/Screens/ReportScreen/ReportViewModel.swift +++ b/AutoCat/Screens/ReportScreen/ReportViewModel.swift @@ -16,10 +16,44 @@ class ReportViewModel { @ObservationIgnored @Service var api: ApiServiceProtocol @ObservationIgnored @Service var storageService: StorageServiceProtocol + var coordinator: ReportCoordinator? + var vehicle: VehicleDto var hud: ACHud? + var plateNumber: String { + if vehicle.outdated, let current = vehicle.currentNumber { + "\(vehicle.number) (\(current))" + } else { + vehicle.number + } + } + + var steerignWheelPosition: String { + vehicle.isRightWheel == true + ? NSLocalizedString("Right", comment: "") + : NSLocalizedString("Left", comment: "") + } + + var isJapanese: String { + vehicle.isJapanese == true + ? NSLocalizedString("Yes", comment: "") + : NSLocalizedString("No", comment: "") + } + init(vehicle: VehicleDto) { self.vehicle = vehicle } + + func openEvents() { + coordinator?.openEvents(vehicle: vehicle) + } + + func openOsago() { + coordinator?.openOsago(contracts: vehicle.osagoContracts) + } + + func openOwners() { + coordinator?.openOwners(ownerships: vehicle.ownershipPeriods) + } } diff --git a/AutoCat/SwiftUI/Binding+Map.swift b/AutoCat/SwiftUI/Binding+Map.swift new file mode 100644 index 0000000..93d1230 --- /dev/null +++ b/AutoCat/SwiftUI/Binding+Map.swift @@ -0,0 +1,20 @@ +// +// Binding+Map.swift +// AutoCat +// +// Created by Selim Mustafaev on 29.11.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI + +extension Binding where Value: Sendable { + + func map(_ transform: @escaping @Sendable (Value) -> Value) -> Binding { + + Binding( + get: { transform(wrappedValue) }, + set: { wrappedValue = transform($0) } + ) + } +} diff --git a/AutoCat/SwiftUI/NavigationLink.swift b/AutoCat/SwiftUI/NavigationLink.swift index c192f3a..d29dad4 100644 --- a/AutoCat/SwiftUI/NavigationLink.swift +++ b/AutoCat/SwiftUI/NavigationLink.swift @@ -10,26 +10,30 @@ import SwiftUI struct NavigationLinkModifier: ViewModifier { - var onTap: () -> Void + var onTap: (() -> Void)? func body(content: Content) -> some View { - HStack(spacing: 0) { + if let onTap { + HStack(spacing: 0) { + content + Spacer() + Image(systemName: "chevron.right") + .font(.footnote) + .fontWeight(.semibold) + .foregroundColor(.secondary) + } + .contentShape(Rectangle()) + .onTapGesture(perform: onTap) + } else { content - Spacer() - Image(systemName: "chevron.right") - .font(.footnote) - .fontWeight(.semibold) - .foregroundColor(.secondary) } - .contentShape(Rectangle()) - .onTapGesture(perform: onTap) } } extension View { - func navigationLink(onTap: @escaping () -> Void) -> some View { + func navigationLink(onTap: (() -> Void)?) -> some View { modifier(NavigationLinkModifier(onTap: onTap)) } } diff --git a/AutoCatCore/Extensions/NullifyDate.swift b/AutoCatCore/Extensions/NullifyDate.swift new file mode 100644 index 0000000..ab2fbb3 --- /dev/null +++ b/AutoCatCore/Extensions/NullifyDate.swift @@ -0,0 +1,31 @@ +// +// NullifyDate.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 30.11.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +@propertyWrapper +public struct NullifyDate: Sendable { + + public var wrappedValue: Date? { + didSet { + wrappedValue = nullifyTime(of: wrappedValue) + } + } + + public init(wrappedValue: Date?) { + self.wrappedValue = nullifyTime(of: wrappedValue) + } + + func nullifyTime(of date: Date?) -> Date? { + guard let date else { + return nil + } + + return Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: date) + } +} diff --git a/AutoCatCore/Models/Filter.swift b/AutoCatCore/Models/Filter.swift index 71be014..f994823 100644 --- a/AutoCatCore/Models/Filter.swift +++ b/AutoCatCore/Models/Filter.swift @@ -98,12 +98,12 @@ public struct Filter: Sendable { public var addedBy: AddedBy = .anyone public var sortBy: SortParameter = .updatedDate public var sortOrder: SortOrder = .descending - public var fromDate: Date? - public var toDate: Date? - public var fromDateUpdated: Date? - public var toDateUpdated: Date? - public var fromLocationDate: Date? - public var toLocationDate: Date? + @NullifyDate public var fromDate: Date? + @NullifyDate public var toDate: Date? + @NullifyDate public var fromDateUpdated: Date? + @NullifyDate public var toDateUpdated: Date? + @NullifyDate public var fromLocationDate: Date? + @NullifyDate public var toLocationDate: Date? public var needReset: Bool = false public var scope: SearchScope = .plateNumber diff --git a/AutoCatTests/FiltersTests.swift b/AutoCatTests/FiltersTests.swift index 4ed7fd0..f3752e4 100644 --- a/AutoCatTests/FiltersTests.swift +++ b/AutoCatTests/FiltersTests.swift @@ -6,6 +6,7 @@ // Copyright © 2024 Selim Mustafaev. All rights reserved. // +import Foundation import Testing import AutoCatCore import Mockable @@ -114,4 +115,28 @@ struct FiltersTests { #expect(viewModel.filter.year == .any) #expect(viewModel.filter.model == .any) } + + @Test("Nullify time in dates") + func nullifyTimeInDates() async throws { + + let testTimestamp: TimeInterval = 1732958508 + let nullifiedTimestamp: TimeInterval = 1732914000 + + // Date with non-zero time components + let date = Date(timeIntervalSince1970: testTimestamp) + + viewModel.filter.fromDate = date + viewModel.filter.toDate = date + viewModel.filter.fromDateUpdated = date + viewModel.filter.toDateUpdated = date + viewModel.filter.fromLocationDate = date + viewModel.filter.toLocationDate = date + + #expect(viewModel.filter.fromDate?.timeIntervalSince1970 == nullifiedTimestamp) + #expect(viewModel.filter.toDate?.timeIntervalSince1970 == nullifiedTimestamp) + #expect(viewModel.filter.fromDateUpdated?.timeIntervalSince1970 == nullifiedTimestamp) + #expect(viewModel.filter.toDateUpdated?.timeIntervalSince1970 == nullifiedTimestamp) + #expect(viewModel.filter.fromLocationDate?.timeIntervalSince1970 == nullifiedTimestamp) + #expect(viewModel.filter.toLocationDate?.timeIntervalSince1970 == nullifiedTimestamp) + } }