diff --git a/mobile-courier-app.xcodeproj/project.pbxproj b/mobile-courier-app.xcodeproj/project.pbxproj index 20c1a7c..63c45c6 100644 --- a/mobile-courier-app.xcodeproj/project.pbxproj +++ b/mobile-courier-app.xcodeproj/project.pbxproj @@ -9,6 +9,17 @@ /* Begin PBXBuildFile section */ 1B0557F62BE9074100367124 /* AuthEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B0557F52BE9074100367124 /* AuthEndpoints.swift */; }; 1B07BC8D2BFD677900D8B149 /* RippleSpinnerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B07BC8C2BFD677900D8B149 /* RippleSpinnerView.swift */; }; + 1B2DDEF22C1E7C95003EC97C /* PackagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2DDEF12C1E7C95003EC97C /* PackagesView.swift */; }; + 1B2DDEF42C1E7DD4003EC97C /* PackagePlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2DDEF32C1E7DD4003EC97C /* PackagePlaceholderView.swift */; }; + 1B2DDEF62C1E9202003EC97C /* PackagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2DDEF52C1E9202003EC97C /* PackagesViewModel.swift */; }; + 1B2DDEF82C1E93B6003EC97C /* PackagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2DDEF72C1E93B6003EC97C /* PackagesModel.swift */; }; + 1B2DDEFA2C1E951A003EC97C /* PackageEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2DDEF92C1E951A003EC97C /* PackageEntity.swift */; }; + 1B2DDEFC2C1E956C003EC97C /* PackagesRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2DDEFB2C1E956C003EC97C /* PackagesRepositoryProtocol.swift */; }; + 1B2DDEFE2C1E95D3003EC97C /* PackagesRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2DDEFD2C1E95D3003EC97C /* PackagesRepository.swift */; }; + 1B2DDF002C1E96B8003EC97C /* PackageEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2DDEFF2C1E96B8003EC97C /* PackageEndpoints.swift */; }; + 1B2DDF022C1E9AC8003EC97C /* PackagesViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2DDF012C1E9AC8003EC97C /* PackagesViewModelMock.swift */; }; + 1B2DDF042C1E9B12003EC97C /* PackagesRepositoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2DDF032C1E9B12003EC97C /* PackagesRepositoryMock.swift */; }; + 1B2DDF062C1E9F85003EC97C /* GroupedPackageRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2DDF052C1E9F85003EC97C /* GroupedPackageRowView.swift */; }; 1B58EF772BE6BBE90066F447 /* MobileCourierApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B58EF762BE6BBE90066F447 /* MobileCourierApp.swift */; }; 1B58EF792BE6BBE90066F447 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B58EF782BE6BBE90066F447 /* MainView.swift */; }; 1B58EF7B2BE6BBEA0066F447 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B58EF7A2BE6BBEA0066F447 /* Assets.xcassets */; }; @@ -23,6 +34,7 @@ 1B58EFAE2BE6C2BE0066F447 /* CourierTextFieldStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B58EFAD2BE6C2BE0066F447 /* CourierTextFieldStyle.swift */; }; 1B58EFB92BE729D20066F447 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B58EFB82BE729D20066F447 /* Endpoint.swift */; }; 1B5EBFFE2C081DDA003BC2D9 /* AuthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5EBFFD2C081DDA003BC2D9 /* AuthData.swift */; }; + 1B6E3D892C211EEF002619C9 /* SinglePackageRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E3D882C211EEF002619C9 /* SinglePackageRowView.swift */; }; 1B821F192C1AA50600ED1795 /* CustomSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B821F182C1AA50600ED1795 /* CustomSessionDelegate.swift */; }; 1B85000E2C10033D006E96A0 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B85000D2C10033D006E96A0 /* ToastView.swift */; }; 1B8500102C1004D0006E96A0 /* ToastModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B85000F2C1004D0006E96A0 /* ToastModifier.swift */; }; @@ -75,6 +87,17 @@ /* Begin PBXFileReference section */ 1B0557F52BE9074100367124 /* AuthEndpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthEndpoints.swift; sourceTree = ""; }; 1B07BC8C2BFD677900D8B149 /* RippleSpinnerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RippleSpinnerView.swift; sourceTree = ""; }; + 1B2DDEF12C1E7C95003EC97C /* PackagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagesView.swift; sourceTree = ""; }; + 1B2DDEF32C1E7DD4003EC97C /* PackagePlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagePlaceholderView.swift; sourceTree = ""; }; + 1B2DDEF52C1E9202003EC97C /* PackagesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagesViewModel.swift; sourceTree = ""; }; + 1B2DDEF72C1E93B6003EC97C /* PackagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagesModel.swift; sourceTree = ""; }; + 1B2DDEF92C1E951A003EC97C /* PackageEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageEntity.swift; sourceTree = ""; }; + 1B2DDEFB2C1E956C003EC97C /* PackagesRepositoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagesRepositoryProtocol.swift; sourceTree = ""; }; + 1B2DDEFD2C1E95D3003EC97C /* PackagesRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagesRepository.swift; sourceTree = ""; }; + 1B2DDEFF2C1E96B8003EC97C /* PackageEndpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageEndpoints.swift; sourceTree = ""; }; + 1B2DDF012C1E9AC8003EC97C /* PackagesViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagesViewModelMock.swift; sourceTree = ""; }; + 1B2DDF032C1E9B12003EC97C /* PackagesRepositoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagesRepositoryMock.swift; sourceTree = ""; }; + 1B2DDF052C1E9F85003EC97C /* GroupedPackageRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedPackageRowView.swift; sourceTree = ""; }; 1B58EF732BE6BBE90066F447 /* mobile-courier-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "mobile-courier-app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 1B58EF762BE6BBE90066F447 /* MobileCourierApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileCourierApp.swift; sourceTree = ""; }; 1B58EF782BE6BBE90066F447 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; @@ -93,6 +116,7 @@ 1B58EFAD2BE6C2BE0066F447 /* CourierTextFieldStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourierTextFieldStyle.swift; sourceTree = ""; }; 1B58EFB82BE729D20066F447 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; 1B5EBFFD2C081DDA003BC2D9 /* AuthData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthData.swift; sourceTree = ""; }; + 1B6E3D882C211EEF002619C9 /* SinglePackageRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SinglePackageRowView.swift; sourceTree = ""; }; 1B821F182C1AA50600ED1795 /* CustomSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSessionDelegate.swift; sourceTree = ""; }; 1B85000D2C10033D006E96A0 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; 1B85000F2C1004D0006E96A0 /* ToastModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastModifier.swift; sourceTree = ""; }; @@ -157,10 +181,22 @@ 1B07BC8C2BFD677900D8B149 /* RippleSpinnerView.swift */, 1B85000D2C10033D006E96A0 /* ToastView.swift */, 1B8B0A432C1BDDE600D1A7AC /* HeaderView.swift */, + 1B2DDEF32C1E7DD4003EC97C /* PackagePlaceholderView.swift */, ); path = Helpers; sourceTree = ""; }; + 1B2DDEF02C1E652A003EC97C /* Package */ = { + isa = PBXGroup; + children = ( + 1B2DDEF12C1E7C95003EC97C /* PackagesView.swift */, + 1B2DDEF52C1E9202003EC97C /* PackagesViewModel.swift */, + 1B2DDF052C1E9F85003EC97C /* GroupedPackageRowView.swift */, + 1B6E3D882C211EEF002619C9 /* SinglePackageRowView.swift */, + ); + path = Package; + sourceTree = ""; + }; 1B58EF6A2BE6BBE90066F447 = { isa = PBXGroup; children = ( @@ -291,6 +327,7 @@ children = ( 1BE63F6E2BE972A60016A26E /* AuthRepositoryProtocol.swift */, 1B9274D12C115E53008F4FA3 /* AddressRepositoryProtocol.swift */, + 1B2DDEFB2C1E956C003EC97C /* PackagesRepositoryProtocol.swift */, ); path = Repositories; sourceTree = ""; @@ -300,6 +337,7 @@ children = ( 1B85E8E12BFC4DAE003040CC /* LoginModel.swift */, 1B9274CF2C115AF1008F4FA3 /* AddressesModel.swift */, + 1B2DDEF72C1E93B6003EC97C /* PackagesModel.swift */, ); path = Models; sourceTree = ""; @@ -328,6 +366,7 @@ children = ( 1B85E8E62BFC4E24003040CC /* LoginEntity.swift */, 1B9274D32C115EA4008F4FA3 /* AddressesEntity.swift */, + 1B2DDEF92C1E951A003EC97C /* PackageEntity.swift */, ); path = Entities; sourceTree = ""; @@ -337,6 +376,7 @@ children = ( 1B85E8E92BFC4E85003040CC /* AuthRepository.swift */, 1B99600E2C12992F00976BBC /* AddressRespository.swift */, + 1B2DDEFD2C1E95D3003EC97C /* PackagesRepository.swift */, ); path = ConcreteRepositories; sourceTree = ""; @@ -344,6 +384,7 @@ 1B85E8EB2BFC4EAB003040CC /* Presentation */ = { isa = PBXGroup; children = ( + 1B2DDEF02C1E652A003EC97C /* Package */, 1B8500132C100DE2006E96A0 /* Profile */, 1BD17E612C0EB42B009B6C67 /* Navigation */, 1BE58CB12C0585B400C7DF63 /* Home */, @@ -360,6 +401,7 @@ 1B58EFB82BE729D20066F447 /* Endpoint.swift */, 1B0557F52BE9074100367124 /* AuthEndpoints.swift */, 1B9274CC2C1158E0008F4FA3 /* AddressEndpoints.swift */, + 1B2DDEFF2C1E96B8003EC97C /* PackageEndpoints.swift */, ); path = Endpoints; sourceTree = ""; @@ -387,6 +429,8 @@ 1BE58CA92C05731B00C7DF63 /* LoginViewModelMock.swift */, 1BC071792C140C6A006EC08E /* ProfileViewModelMock.swift */, 1BC0717B2C140CA9006EC08E /* AddressesRepositoryMock.swift */, + 1B2DDF012C1E9AC8003EC97C /* PackagesViewModelMock.swift */, + 1B2DDF032C1E9B12003EC97C /* PackagesRepositoryMock.swift */, ); path = Previews; sourceTree = ""; @@ -578,6 +622,7 @@ files = ( 1B99600F2C12992F00976BBC /* AddressRespository.swift in Sources */, 1B58EFB92BE729D20066F447 /* Endpoint.swift in Sources */, + 1B2DDEF42C1E7DD4003EC97C /* PackagePlaceholderView.swift in Sources */, 1B07BC8D2BFD677900D8B149 /* RippleSpinnerView.swift in Sources */, 1B9274D02C115AF1008F4FA3 /* AddressesModel.swift in Sources */, 1BE58CA82C0572B400C7DF63 /* StorageMock.swift in Sources */, @@ -586,8 +631,12 @@ 1B9274CB2C1156C6008F4FA3 /* AppData.swift in Sources */, 1BE58CAA2C05731B00C7DF63 /* LoginViewModelMock.swift in Sources */, 1BE63F712BE980D60016A26E /* Storage.swift in Sources */, + 1B2DDEFC2C1E956C003EC97C /* PackagesRepositoryProtocol.swift in Sources */, 1B85000E2C10033D006E96A0 /* ToastView.swift in Sources */, + 1B2DDEF82C1E93B6003EC97C /* PackagesModel.swift in Sources */, 1B8500102C1004D0006E96A0 /* ToastModifier.swift in Sources */, + 1B2DDEF62C1E9202003EC97C /* PackagesViewModel.swift in Sources */, + 1B2DDF002C1E96B8003EC97C /* PackageEndpoints.swift in Sources */, 1B58EFA52BE6BFBB0066F447 /* LoginViewModel.swift in Sources */, 1B9960112C129BFB00976BBC /* ProfileViewModel.swift in Sources */, 1B58EFAE2BE6C2BE0066F447 /* CourierTextFieldStyle.swift in Sources */, @@ -598,7 +647,10 @@ 1B9274D22C115E53008F4FA3 /* AddressRepositoryProtocol.swift in Sources */, 1B58EF792BE6BBE90066F447 /* MainView.swift in Sources */, 1BE63F6D2BE9193E0016A26E /* APIRequestClient.swift in Sources */, + 1B2DDF042C1E9B12003EC97C /* PackagesRepositoryMock.swift in Sources */, + 1B2DDEFA2C1E951A003EC97C /* PackageEntity.swift in Sources */, 1BE63F6F2BE972A60016A26E /* AuthRepositoryProtocol.swift in Sources */, + 1B2DDEF22C1E7C95003EC97C /* PackagesView.swift in Sources */, 1B58EFA92BE6C0070066F447 /* BorderedViewModifier.swift in Sources */, 1BC0717C2C140CA9006EC08E /* AddressesRepositoryMock.swift in Sources */, 1B821F192C1AA50600ED1795 /* CustomSessionDelegate.swift in Sources */, @@ -608,10 +660,14 @@ 1B9274CD2C1158E0008F4FA3 /* AddressEndpoints.swift in Sources */, 1B85E8E72BFC4E24003040CC /* LoginEntity.swift in Sources */, 1B58EF772BE6BBE90066F447 /* MobileCourierApp.swift in Sources */, + 1B2DDF022C1E9AC8003EC97C /* PackagesViewModelMock.swift in Sources */, + 1B2DDEFE2C1E95D3003EC97C /* PackagesRepository.swift in Sources */, 1B5EBFFE2C081DDA003BC2D9 /* AuthData.swift in Sources */, 1BC0717A2C140C6A006EC08E /* ProfileViewModelMock.swift in Sources */, + 1B6E3D892C211EEF002619C9 /* SinglePackageRowView.swift in Sources */, 1BE58CA62C05727500C7DF63 /* AuthRepositoryMock.swift in Sources */, 1BE58CB32C0585C400C7DF63 /* HomeView.swift in Sources */, + 1B2DDF062C1E9F85003EC97C /* GroupedPackageRowView.swift in Sources */, 1BD17E632C0EB438009B6C67 /* Coordinator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/mobile-courier-app/Assets.xcassets/surface.colorset/Contents.json b/mobile-courier-app/Assets.xcassets/surface.colorset/Contents.json new file mode 100644 index 0000000..e5f8e60 --- /dev/null +++ b/mobile-courier-app/Assets.xcassets/surface.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF1", + "green" : "0xE1", + "red" : "0xE2" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x52", + "green" : "0x46", + "red" : "0x45" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile-courier-app/Data/ConcreteRepositories/PackagesRepository.swift b/mobile-courier-app/Data/ConcreteRepositories/PackagesRepository.swift new file mode 100644 index 0000000..2b8d1b9 --- /dev/null +++ b/mobile-courier-app/Data/ConcreteRepositories/PackagesRepository.swift @@ -0,0 +1,25 @@ +// +// PackagesRepository.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-15. +// + +import Foundation + +struct PackagesRepository: PackagesRepositoryProtocol { + private var apiRequestClient: APIRequestClientProtocol + + init(apiRequestClient: APIRequestClientProtocol) { + self.apiRequestClient = apiRequestClient + } + + func getPackagesRetrieved() async throws -> [GroupedPackageEntity] { + let model: PackagesModel = try await apiRequestClient.performRequest( + endpoint: PackageEndpoints.retrieved, + decoder: JSONDecoder() + ) + + return model.asEntity() + } +} diff --git a/mobile-courier-app/Data/Networking/Endpoints/Endpoint.swift b/mobile-courier-app/Data/Networking/Endpoints/Endpoint.swift index 613d770..bee453b 100644 --- a/mobile-courier-app/Data/Networking/Endpoints/Endpoint.swift +++ b/mobile-courier-app/Data/Networking/Endpoints/Endpoint.swift @@ -38,7 +38,7 @@ extension Endpoint { ] if let authToken = authToken { - internalHeaders["Authorization"] = "Bearer \(authToken)" + internalHeaders["Authorization"] = authToken } return internalHeaders diff --git a/mobile-courier-app/Data/Networking/Endpoints/PackageEndpoints.swift b/mobile-courier-app/Data/Networking/Endpoints/PackageEndpoints.swift new file mode 100644 index 0000000..382bc8d --- /dev/null +++ b/mobile-courier-app/Data/Networking/Endpoints/PackageEndpoints.swift @@ -0,0 +1,34 @@ +// +// PackageEndpoints.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-15. +// + +import Foundation + +enum PackageEndpoints { + case retrieved +} + +extension PackageEndpoints: Endpoint { + var mockFile: String? { + "" + } + + var requestType: RequestType { + .get + } + + var path: String { + "/frontliner-middleware/api/paquetesRetirados" + } + + var body: [AnyHashable: Any]? { + nil + } + + var queryParams: [String: String]? { + nil + } +} diff --git a/mobile-courier-app/Data/Networking/Models/PackagesModel.swift b/mobile-courier-app/Data/Networking/Models/PackagesModel.swift new file mode 100644 index 0000000..a8529ac --- /dev/null +++ b/mobile-courier-app/Data/Networking/Models/PackagesModel.swift @@ -0,0 +1,79 @@ +// +// PackagesModel.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-15. +// + +import Foundation + +struct PackagesModel: Codable { + let paquetes: [PackageModel] +} + +struct PackageModel: Codable { + let estado: String + let embarqueEstado: String + let paqueteFechaRetiro: String + let embarqueMedio: String + let tarifaPrecioCli: Decimal + let paqueteDescripcion: String + let paqueteTracking: String + let cotizacion: Decimal + let embarqueFecha: String + let paquetePeso: Decimal + let embarqueCodigo: Int + let id: Int + let paquetePrecio: Decimal + + enum CodingKeys: String, CodingKey { + case estado + case embarqueEstado = "embarqueestado" + case paqueteFechaRetiro = "paquetefecharetiro" + case embarqueMedio = "embarquemedio" + case tarifaPrecioCli = "tarifapreciocli" + case paqueteDescripcion = "paquetedescripcion" + case paqueteTracking = "paquetetracking" + case cotizacion + case embarqueFecha = "embarquefecha" + case paquetePeso = "paquetepeso" + case embarqueCodigo = "embarquecodigo" + case id + case paquetePrecio = "paqueteprecio" + } +} + +extension PackageModel { + func asEntity() -> PackageEntity { + .init( + estado: estado, + embarqueEstado: embarqueEstado, + paqueteFechaRetiro: paqueteFechaRetiro, + embarqueMedio: embarqueMedio, + tarifaPrecioCli: tarifaPrecioCli, + paqueteDescripcion: paqueteDescripcion, + paqueteTracking: paqueteTracking, + cotizacion: cotizacion, + embarqueFecha: embarqueFecha, + paquetePeso: paquetePeso, + embarqueCodigo: embarqueCodigo, + id: id, + paquetePrecio: paquetePrecio + ) + } +} + +extension PackagesModel { + func asEntity() -> [GroupedPackageEntity] { + Dictionary(grouping: paquetes, by: { $0.embarqueCodigo }) + .sorted(by: { + Int($0.key) > Int($1.key) + }) + .map { + GroupedPackageEntity( + embarqueCodigo: $0.key, + paquetes: $0.value.map { $0.asEntity() } + ) + } + } +} diff --git a/mobile-courier-app/Domain/Entities/PackageEntity.swift b/mobile-courier-app/Domain/Entities/PackageEntity.swift new file mode 100644 index 0000000..eb8947c --- /dev/null +++ b/mobile-courier-app/Domain/Entities/PackageEntity.swift @@ -0,0 +1,91 @@ +// +// PackageEntity.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-15. +// + +import Foundation + +struct GroupedPackageEntity: Identifiable { + var id: Int { embarqueCodigo } + + var embarqueCodigo: Int + var paquetes: [PackageEntity] + + var totalCost: Decimal { + let thousand: Decimal = 1000 + let cost = paquetes.reduce(.zero) { + $0 + $1.guaraniesCost + } + + let divided = cost / thousand + let rounded = NSDecimalNumber(decimal: divided).rounding(accordingToBehavior: nil) + return rounded.multiplying(by: thousand as NSDecimalNumber) as Decimal + } + + var totalWeight: Decimal { + paquetes.reduce(.zero) { + $0 + $1.paquetePeso + } + } + + var formattedTotalCost: String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.minimumFractionDigits = .zero + formatter.allowsFloats = false + formatter.groupingSeparator = "." + + let formattedValue = NSDecimalNumber(decimal: totalCost) + return formatter.string(from: formattedValue) ?? "0" + } + + var formattedDate: String { + let inputDateFormatter = DateFormatter() + inputDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + + guard let firstDate = paquetes.first?.embarqueFecha, + let date = inputDateFormatter.date(from: firstDate) else { return "" } + + let outputDateFormatter = DateFormatter() + outputDateFormatter.dateFormat = "MMMM d, yyyy" + + let formattedDateString = outputDateFormatter.string(from: date) + + return formattedDateString + } + + var formattedId: String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.minimumFractionDigits = .zero + formatter.allowsFloats = false + formatter.groupingSeparator = "" + + let formattedValue = NSDecimalNumber(integerLiteral: embarqueCodigo) + return formatter.string(from: formattedValue) ?? "0" + } +} + +struct PackageEntity: Codable, Identifiable { + let estado: String + let embarqueEstado: String + let paqueteFechaRetiro: String + let embarqueMedio: String + let tarifaPrecioCli: Decimal + let paqueteDescripcion: String + let paqueteTracking: String + let cotizacion: Decimal + let embarqueFecha: String + let paquetePeso: Decimal + let embarqueCodigo: Int + let id: Int + let paquetePrecio: Decimal +} + +extension PackageEntity { + var guaraniesCost: Decimal { + tarifaPrecioCli * cotizacion * paquetePeso + } +} diff --git a/mobile-courier-app/Domain/Repositories/PackagesRepositoryProtocol.swift b/mobile-courier-app/Domain/Repositories/PackagesRepositoryProtocol.swift new file mode 100644 index 0000000..0a13001 --- /dev/null +++ b/mobile-courier-app/Domain/Repositories/PackagesRepositoryProtocol.swift @@ -0,0 +1,12 @@ +// +// PackagesRepositoryProtocol.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-15. +// + +import Foundation + +protocol PackagesRepositoryProtocol { + func getPackagesRetrieved() async throws -> [GroupedPackageEntity] +} diff --git a/mobile-courier-app/Presentation/Helpers/PackagePlaceholderView.swift b/mobile-courier-app/Presentation/Helpers/PackagePlaceholderView.swift new file mode 100644 index 0000000..c9bec4b --- /dev/null +++ b/mobile-courier-app/Presentation/Helpers/PackagePlaceholderView.swift @@ -0,0 +1,35 @@ +// +// PackagePlaceholderView.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-15. +// + +import SwiftUI + +struct PackagePlaceholderView: View { + var body: some View { + VStack { + Image(systemName: "shippingbox.circle.fill") + .resizable() + .aspectRatio(1, contentMode: .fit) + .frame(height: 80) + .foregroundStyle(.accent) + .padding(.bottom, 8) + + VStack(alignment: .center) { + Text("You have no packages") + .padding(.bottom, 4) + .fontWeight(.bold) + .foregroundStyle(.accent) + Text("Your packages will be displayed here when they arrive at your mailbox") + } + .multilineTextAlignment(.center) + .padding(20) + } + } +} + +#Preview { + PackagePlaceholderView() +} diff --git a/mobile-courier-app/Presentation/Home/HomeView.swift b/mobile-courier-app/Presentation/Home/HomeView.swift index 7561857..17cbce8 100644 --- a/mobile-courier-app/Presentation/Home/HomeView.swift +++ b/mobile-courier-app/Presentation/Home/HomeView.swift @@ -18,12 +18,12 @@ struct HomeView: View { HeaderView() TabView { - EmptyView() + coordinator.build(page: .package) .tabItem { Label("Home", systemImage: "house") } - EmptyView() + coordinator.build(page: .package) .tabItem { Label("Withdrawn", systemImage: "bag") } diff --git a/mobile-courier-app/Presentation/Login/LoginView.swift b/mobile-courier-app/Presentation/Login/LoginView.swift index 917991f..8c5c111 100644 --- a/mobile-courier-app/Presentation/Login/LoginView.swift +++ b/mobile-courier-app/Presentation/Login/LoginView.swift @@ -82,6 +82,6 @@ struct LoginView: View { #Preview { LoginView( - viewModel: LoginViewModel.previewInstance() + viewModel: .previewInstance() ) } diff --git a/mobile-courier-app/Presentation/Navigation/Coordinator.swift b/mobile-courier-app/Presentation/Navigation/Coordinator.swift index 951a241..5703f9e 100644 --- a/mobile-courier-app/Presentation/Navigation/Coordinator.swift +++ b/mobile-courier-app/Presentation/Navigation/Coordinator.swift @@ -11,6 +11,7 @@ enum Page: String, Identifiable { case login case home case profile + case package var id: String { self.rawValue @@ -53,17 +54,20 @@ final class Coordinator: ObservableObject { @ViewBuilder func build(page: Page) -> some View { + let apiClient = APIRequestClient() + switch page { case .login: - let apiClient = APIRequestClient() let authRepo = AuthRepository(apiRequestClient: apiClient) - LoginView(viewModel: LoginViewModel(authRepository: authRepo)) + LoginView(viewModel: .init(authRepository: authRepo)) case .profile: - let apiClient = APIRequestClient() let addressRepo = AddressRespository(apiRequestClient: apiClient) - ProfileView(viewModel: ProfileViewModel(addressesRepository: addressRepo)) + ProfileView(viewModel: .init(addressesRepository: addressRepo)) case .home: HomeView() + case .package: + let packagesRepo = PackagesRepository(apiRequestClient: apiClient) + PackagesView(viewModel: .init(packagesRepository: packagesRepo)) } } diff --git a/mobile-courier-app/Presentation/Package/GroupedPackageRowView.swift b/mobile-courier-app/Presentation/Package/GroupedPackageRowView.swift new file mode 100644 index 0000000..6c237fd --- /dev/null +++ b/mobile-courier-app/Presentation/Package/GroupedPackageRowView.swift @@ -0,0 +1,73 @@ +// +// PackageRowView.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-16. +// + +import SwiftUI + +struct GroupedPackageRowView: View { + + var groupedPackage: GroupedPackageEntity + + var body: some View { + VStack(spacing: 8) { + HStack(alignment: .top) { + VStack { + Text("N° \(groupedPackage.formattedId)") + .padding(4) + .foregroundStyle(.white) + .background( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(.accent) + ) + } + + Spacer() + + VStack(alignment: .trailing) { + Text("Gs. \(groupedPackage.formattedTotalCost)") + .foregroundStyle(.accent) + .fontWeight(.bold) + Text("\(groupedPackage.totalWeight) Kg") + } + } + + HStack { + Text(groupedPackage.formattedDate) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Text("\(groupedPackage.paquetes.count) packages") + } + + } + .frame(maxWidth: .infinity) + .padding(.init( + top: 8, + leading: 16, + bottom: 8, + trailing: 16) + ) + .foregroundStyle(.black) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(Color.surface) + ) + .clipped() + .listRowSeparator(.hidden) + .listRowSpacing(.zero) + .listRowInsets(.none) + } +} + +#Preview { + GroupedPackageRowView( + groupedPackage: .init( + embarqueCodigo: 2000, + paquetes: [.mock] + ) + ) +} diff --git a/mobile-courier-app/Presentation/Package/PackagesView.swift b/mobile-courier-app/Presentation/Package/PackagesView.swift new file mode 100644 index 0000000..57f9fa3 --- /dev/null +++ b/mobile-courier-app/Presentation/Package/PackagesView.swift @@ -0,0 +1,43 @@ +// +// PackagesView.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-15. +// + +import SwiftUI + +struct PackagesView: View { + @ObservedObject var viewModel: PackagesViewModel + + var body: some View { + ZStack { + VStack { + if let groupedPackagesEntity = viewModel.groupedPackagesEntity { + if groupedPackagesEntity.isEmpty { + PackagePlaceholderView() + } else { + List(groupedPackagesEntity) { row in + GroupedPackageRowView(groupedPackage: row) + } + .listStyle(.plain) + } + } + } + + if viewModel.isLoading { + RippleSpinnerView() + } + } + .onAppear { + Task { + await viewModel.getPackages() + } + } + .toast(message: $viewModel.toastMessage) + } +} + +#Preview { + PackagesView(viewModel: .previewInstance()) +} diff --git a/mobile-courier-app/Presentation/Package/PackagesViewModel.swift b/mobile-courier-app/Presentation/Package/PackagesViewModel.swift new file mode 100644 index 0000000..133251c --- /dev/null +++ b/mobile-courier-app/Presentation/Package/PackagesViewModel.swift @@ -0,0 +1,32 @@ +// +// PackagesViewModel.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-15. +// + +import Foundation + +final class PackagesViewModel: ObservableObject { + @Published var isLoading: Bool = false + @Published var toastMessage: String? + @Published var groupedPackagesEntity: [GroupedPackageEntity]? + + private let packagesRepository: PackagesRepositoryProtocol + + init(packagesRepository: PackagesRepositoryProtocol) { + self.packagesRepository = packagesRepository + } + + @MainActor + func getPackages() async { + do { + isLoading = true + groupedPackagesEntity = try await packagesRepository.getPackagesRetrieved() + isLoading = false + } catch { + isLoading = false + toastMessage = error.localizedDescription + } + } +} diff --git a/mobile-courier-app/Presentation/Package/SinglePackageRowView.swift b/mobile-courier-app/Presentation/Package/SinglePackageRowView.swift new file mode 100644 index 0000000..e341fef --- /dev/null +++ b/mobile-courier-app/Presentation/Package/SinglePackageRowView.swift @@ -0,0 +1,20 @@ +// +// SinglePackageRowView.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-17. +// + +import SwiftUI + +struct SinglePackageRowView: View { + var package: PackageEntity + + var body: some View { + Text(package.paqueteDescripcion) + } +} + +#Preview { + SinglePackageRowView(package: .mock) +} diff --git a/mobile-courier-app/Presentation/Previews/PackagesRepositoryMock.swift b/mobile-courier-app/Presentation/Previews/PackagesRepositoryMock.swift new file mode 100644 index 0000000..2af1c7b --- /dev/null +++ b/mobile-courier-app/Presentation/Previews/PackagesRepositoryMock.swift @@ -0,0 +1,35 @@ +// +// PackagesRepositoryMock.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-16. +// + +import Foundation + +struct PackagesRepositoryMock: PackagesRepositoryProtocol { + func getPackagesRetrieved() async throws -> [GroupedPackageEntity] { + [ + .init(embarqueCodigo: 2143, paquetes: [ .mock ]) + ] + } +} + +extension PackageEntity { + static var mock: PackageEntity { + .init( + estado: "c", + embarqueEstado: "c", + paqueteFechaRetiro: "2024-05-10T04:00:00Z", + embarqueMedio: "Air shipment", + tarifaPrecioCli: 22, + paqueteDescripcion: "It's a package", + paqueteTracking: "Tracking", + cotizacion: 7450, + embarqueFecha: "2024-05-10T04:00:00Z", + paquetePeso: 1.2, + embarqueCodigo: 2143, + id: 1, + paquetePrecio: 5000) + } +} diff --git a/mobile-courier-app/Presentation/Previews/PackagesViewModelMock.swift b/mobile-courier-app/Presentation/Previews/PackagesViewModelMock.swift new file mode 100644 index 0000000..b44ed73 --- /dev/null +++ b/mobile-courier-app/Presentation/Previews/PackagesViewModelMock.swift @@ -0,0 +1,14 @@ +// +// PackagesViewModelMock.swift +// mobile-courier-app +// +// Created by Vladimir Espinola on 2024-06-16. +// + +import Foundation + +extension PackagesViewModel { + static func previewInstance() -> PackagesViewModel { + .init(packagesRepository: PackagesRepositoryMock()) + } +} diff --git a/mobile-courier-app/Presentation/Profile/ProfileView.swift b/mobile-courier-app/Presentation/Profile/ProfileView.swift index e1f4eb5..fe02231 100644 --- a/mobile-courier-app/Presentation/Profile/ProfileView.swift +++ b/mobile-courier-app/Presentation/Profile/ProfileView.swift @@ -25,15 +25,17 @@ struct ProfileView: View { EnviosView(envios: addresses.viaMaritima, title: "Maritime Route") } } + .listStyle(.plain) } if viewModel.isLoading { RippleSpinnerView() } } + .toast(message: $viewModel.toastMessage) } } #Preview { - ProfileView(viewModel: ProfileViewModel.previewInstance()) + ProfileView(viewModel: .previewInstance()) } diff --git a/mobile-courier-app/Presentation/Profile/ProfileViewModel.swift b/mobile-courier-app/Presentation/Profile/ProfileViewModel.swift index b0ba91c..2e6cb53 100644 --- a/mobile-courier-app/Presentation/Profile/ProfileViewModel.swift +++ b/mobile-courier-app/Presentation/Profile/ProfileViewModel.swift @@ -27,16 +27,14 @@ final class ProfileViewModel: ObservableObject { } @MainActor - func getAddresses() async -> AddressesEntity? { + func getAddresses() async { do { isLoading = true addresses = try await addressesRepository.getAddress() isLoading = false - return addresses } catch { isLoading = false toastMessage = error.localizedDescription - return nil } } } diff --git a/mobile-courier-app/Utilities/Localizable.xcstrings b/mobile-courier-app/Utilities/Localizable.xcstrings index 7476b46..5dd25e5 100644 --- a/mobile-courier-app/Utilities/Localizable.xcstrings +++ b/mobile-courier-app/Utilities/Localizable.xcstrings @@ -3,6 +3,9 @@ "strings" : { "" : { + }, + "%lld packages" : { + }, "Email" : { "extractionState" : "manual", @@ -21,11 +24,28 @@ } } }, - "Hi, %@!" : { + "Gs. %@" : { + }, + "Hi, %@!" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hola, %@!" + } + } + } }, "Home" : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inicio" + } + } + } }, "Log In" : { "extractionState" : "manual", @@ -43,6 +63,9 @@ } } } + }, + "N° %@" : { + }, "Password" : { "extractionState" : "manual", @@ -62,7 +85,14 @@ } }, "Profile" : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perfil" + } + } + } }, "Remember me" : { "extractionState" : "manual", @@ -82,7 +112,37 @@ } }, "Withdrawn" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retirados" + } + } + } + }, + "Withdrawn Packages" : { + }, + "You have no packages" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usted no tienes paquetes" + } + } + } + }, + "Your packages will be displayed here when they arrive at your mailbox" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sus paquetes serán mostrados aquí cuando lleguen a su casilla" + } + } + } } }, "version" : "1.0" diff --git a/mobile-courier-appTests/LoginViewModelTests.swift b/mobile-courier-appTests/LoginViewModelTests.swift index 65504c4..ecd9c5f 100644 --- a/mobile-courier-appTests/LoginViewModelTests.swift +++ b/mobile-courier-appTests/LoginViewModelTests.swift @@ -20,7 +20,7 @@ final class LoginViewModelTests: XCTestCase { storageSpy = StorageSpy() authRepoSpy = AuthRepositorySpy() - sut = LoginViewModel(authRepository: authRepoSpy, storage: storageSpy) + sut = LoginViewModel(authRepository: authRepoSpy) } override func tearDownWithError() throws { @@ -33,14 +33,7 @@ final class LoginViewModelTests: XCTestCase { } func testLoadingState() async { - await sut.doLogin() + _ = await sut.doLogin() XCTAssertTrue(authRepoSpy.performLoginCalled) } - - func testStorage() async { - await sut.saveUserPreferences(isOn: true) - XCTAssertTrue(storageSpy.setStringCalled) - await sut.saveUserPreferences(isOn: false) - XCTAssertTrue(storageSpy.deleteCalled) - } }