Fixing issue with filtering by dates

This commit is contained in:
Selim Mustafaev 2024-11-30 12:39:51 +03:00
parent 796ad90b6c
commit 46a70ef611
10 changed files with 207 additions and 22 deletions

View File

@ -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 = "<group>"; };
7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecord.swift; sourceTree = "<group>"; };
7A659B5A24A3768A0043A0F2 /* Substrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Substrings.swift; sourceTree = "<group>"; };
7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NullifyDate.swift; sourceTree = "<group>"; };
7A6DD902242BF4A5009DE740 /* PlateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlateView.swift; sourceTree = "<group>"; };
7A6DD90724329144009DE740 /* CenterTextLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenterTextLayer.swift; sourceTree = "<group>"; };
7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers2.0.otf; sourceTree = "<group>"; };
@ -418,6 +421,7 @@
7AAAFAD92C4D1AFE0050410D /* Zoomable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zoomable.swift; sourceTree = "<group>"; };
7AAAFADB2C4D1E130050410D /* ACImageSliderView+Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ACImageSliderView+Modifier.swift"; sourceTree = "<group>"; };
7AAAFADD2C4D23620050410D /* ACImageSliderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACImageSliderModel.swift; sourceTree = "<group>"; };
7AABBE3A2CF9F85600346588 /* Binding+Map.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Map.swift"; sourceTree = "<group>"; };
7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxSectionedDataSource.swift; sourceTree = "<group>"; };
7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = "<group>"; };
7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocationProtocol.swift; sourceTree = "<group>"; };
@ -1058,6 +1062,7 @@
7A27ADF824A09CAD0035F39E /* CocoaError.swift */,
7AE8424D26109F78002F6B31 /* Exportable.swift */,
7A1CF81529A42117007962DA /* Realm.swift */,
7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1075,6 +1080,7 @@
7AF8606F2CBAA24500954D2F /* NavigationLink.swift */,
7A2E11282CCE395300E5CA17 /* OptionalDatePicker.swift */,
7A4927D42CCE438600851C01 /* OptionalBinding.swift */,
7AABBE3A2CF9F85600346588 /* Binding+Map.swift */,
);
path = SwiftUI;
sourceTree = "<group>";
@ -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;

View File

@ -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)
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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<Value> {
Binding<Value>(
get: { transform(wrappedValue) },
set: { wrappedValue = transform($0) }
)
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}
}