diff --git a/SampoomManagement/App/ContentView.swift b/SampoomManagement/App/ContentView.swift index a90ec76..6e49bfc 100644 --- a/SampoomManagement/App/ContentView.swift +++ b/SampoomManagement/App/ContentView.swift @@ -29,7 +29,7 @@ struct ContentView: View { init(dependencies: AppDependencies) { self.dependencies = dependencies _partViewModel = StateObject(wrappedValue: dependencies.makePartViewModel()) - _dashboardViewModel = StateObject(wrappedValue: DashboardViewModel(getOrderUseCase: dependencies.getOrderUseCase)) + _dashboardViewModel = StateObject(wrappedValue: dependencies.makeDashboardViewModel()) } // MARK: - Body diff --git a/SampoomManagement/Core/DI/AppDependencies.swift b/SampoomManagement/Core/DI/AppDependencies.swift index bb3be03..55b9bfa 100644 --- a/SampoomManagement/Core/DI/AppDependencies.swift +++ b/SampoomManagement/Core/DI/AppDependencies.swift @@ -67,6 +67,12 @@ class AppDependencies { let receiveOrderUseCase: ReceiveOrderUseCase let cancelOrderUseCase: CancelOrderUseCase + // MARK: - Dashboard + let dashboardAPI: DashboardAPI + let dashboardRepository: DashboardRepository + let getDashboardUseCase: GetDashboardUseCase + let getWeeklySummaryUseCase: GetWeeklySummaryUseCase + init() { // Global Message Handler globalMessageHandler = GlobalMessageHandler.shared @@ -140,6 +146,12 @@ class AppDependencies { completeOrderUseCase = CompleteOrderUseCase(repository: orderRepository) receiveOrderUseCase = ReceiveOrderUseCase(repository: orderRepository) cancelOrderUseCase = CancelOrderUseCase(repository: orderRepository) + + // Dashboard + dashboardAPI = DashboardAPI(networkManager: networkManager) + dashboardRepository = DashboardRepositoryImpl(api: dashboardAPI, authPreferences: authPreferences) + getDashboardUseCase = GetDashboardUseCase(repository: dashboardRepository) + getWeeklySummaryUseCase = GetWeeklySummaryUseCase(repository: dashboardRepository) } // MARK: - ViewModel Factories @@ -224,5 +236,14 @@ class AppDependencies { globalMessageHandler: globalMessageHandler ) } + + func makeDashboardViewModel() -> DashboardViewModel { + return DashboardViewModel( + getOrderUseCase: getOrderUseCase, + getDashboardUseCase: getDashboardUseCase, + getWeeklySummaryUseCase: getWeeklySummaryUseCase, + messageHandler: globalMessageHandler + ) + } } diff --git a/SampoomManagement/Core/Resources/StringResources.swift b/SampoomManagement/Core/Resources/StringResources.swift index 83a9074..b2b7615 100644 --- a/SampoomManagement/Core/Resources/StringResources.swift +++ b/SampoomManagement/Core/Resources/StringResources.swift @@ -25,6 +25,9 @@ struct StringResources { static let shortageOfParts = "부족 부품" static let orderAmount = "주문 금액" static let recentOrdersTitle = "최근 주문" + static let weeklySummaryTitle = "이번 주 요약" + static let weeklySummaryInStock = "입고 부품" + static let weeklySummaryOutStock = "출고 부품" } // MARK: - Tabs diff --git a/SampoomManagement/Features/Dashboard/Data/Mappers/DashboardMappers.swift b/SampoomManagement/Features/Dashboard/Data/Mappers/DashboardMappers.swift new file mode 100644 index 0000000..ffdf8ef --- /dev/null +++ b/SampoomManagement/Features/Dashboard/Data/Mappers/DashboardMappers.swift @@ -0,0 +1,22 @@ +import Foundation + +extension DashboardResponseDTO { + func toModel() -> Dashboard { + return Dashboard( + totalParts: totalParts, + outOfStockParts: outOfStockParts, + lowStockParts: lowStockParts, + totalQuantity: totalQuantity + ) + } +} + +extension WeeklySummaryResponseDTO { + func toModel() -> WeeklySummary { + return WeeklySummary( + inStockParts: inStockParts, + outStockParts: outStockParts, + weekPeriod: weekPeriod + ) + } +} diff --git a/SampoomManagement/Features/Dashboard/Data/Remote/API/DashboardAPI.swift b/SampoomManagement/Features/Dashboard/Data/Remote/API/DashboardAPI.swift new file mode 100644 index 0000000..e71a007 --- /dev/null +++ b/SampoomManagement/Features/Dashboard/Data/Remote/API/DashboardAPI.swift @@ -0,0 +1,28 @@ +import Foundation +import Alamofire + +class DashboardAPI { + private let networkManager: NetworkManager + + init(networkManager: NetworkManager) { + self.networkManager = networkManager + } + + func getDashboard(agencyId: Int) async throws -> APIResponse { + let endpoint = "agency/\(agencyId)/dashboard" + return try await networkManager.request( + endpoint: endpoint, + method: .get, + responseType: DashboardResponseDTO.self + ) + } + + func getWeeklySummary(agencyId: Int) async throws -> APIResponse { + let endpoint = "agency/\(agencyId)/weekly-summary" + return try await networkManager.request( + endpoint: endpoint, + method: .get, + responseType: WeeklySummaryResponseDTO.self + ) + } +} diff --git a/SampoomManagement/Features/Dashboard/Data/Remote/DTO/DashboardResponseDTO.swift b/SampoomManagement/Features/Dashboard/Data/Remote/DTO/DashboardResponseDTO.swift new file mode 100644 index 0000000..3b78ba1 --- /dev/null +++ b/SampoomManagement/Features/Dashboard/Data/Remote/DTO/DashboardResponseDTO.swift @@ -0,0 +1,8 @@ +import Foundation + +struct DashboardResponseDTO: Codable { + let totalParts: Int + let outOfStockParts: Int + let lowStockParts: Int + let totalQuantity: Int +} diff --git a/SampoomManagement/Features/Dashboard/Data/Remote/DTO/WeeklySummaryResponseDTO.swift b/SampoomManagement/Features/Dashboard/Data/Remote/DTO/WeeklySummaryResponseDTO.swift new file mode 100644 index 0000000..88060e0 --- /dev/null +++ b/SampoomManagement/Features/Dashboard/Data/Remote/DTO/WeeklySummaryResponseDTO.swift @@ -0,0 +1,7 @@ +import Foundation + +struct WeeklySummaryResponseDTO: Codable { + let inStockParts: Int + let outStockParts: Int + let weekPeriod: String +} diff --git a/SampoomManagement/Features/Dashboard/Data/Repository/DashboardRepositoryImpl.swift b/SampoomManagement/Features/Dashboard/Data/Repository/DashboardRepositoryImpl.swift new file mode 100644 index 0000000..4ef4e80 --- /dev/null +++ b/SampoomManagement/Features/Dashboard/Data/Repository/DashboardRepositoryImpl.swift @@ -0,0 +1,27 @@ +import Foundation + +class DashboardRepositoryImpl: DashboardRepository { + private let api: DashboardAPI + private let authPreferences: AuthPreferences + + init(api: DashboardAPI, authPreferences: AuthPreferences) { + self.api = api + self.authPreferences = authPreferences + } + + func getDashboard() async throws -> Dashboard { + guard let user = try authPreferences.getStoredUser() else { throw NetworkError.unauthorized } + let response = try await api.getDashboard(agencyId: user.agencyId) + if !response.success { throw NetworkError.serverError(response.status, message: response.message) } + guard let data = response.data else { throw NetworkError.noData } + return data.toModel() + } + + func getWeeklySummary() async throws -> WeeklySummary { + guard let user = try authPreferences.getStoredUser() else { throw NetworkError.unauthorized } + let response = try await api.getWeeklySummary(agencyId: user.agencyId) + if !response.success { throw NetworkError.serverError(response.status, message: response.message) } + guard let data = response.data else { throw NetworkError.noData } + return data.toModel() + } +} diff --git a/SampoomManagement/Features/Dashboard/Domain/Models/Dashboard.swift b/SampoomManagement/Features/Dashboard/Domain/Models/Dashboard.swift new file mode 100644 index 0000000..651897c --- /dev/null +++ b/SampoomManagement/Features/Dashboard/Domain/Models/Dashboard.swift @@ -0,0 +1,8 @@ +import Foundation + +struct Dashboard: Equatable { + let totalParts: Int + let outOfStockParts: Int + let lowStockParts: Int + let totalQuantity: Int +} diff --git a/SampoomManagement/Features/Dashboard/Domain/Models/WeeklySummary.swift b/SampoomManagement/Features/Dashboard/Domain/Models/WeeklySummary.swift new file mode 100644 index 0000000..83ad0f2 --- /dev/null +++ b/SampoomManagement/Features/Dashboard/Domain/Models/WeeklySummary.swift @@ -0,0 +1,7 @@ +import Foundation + +struct WeeklySummary: Equatable { + let inStockParts: Int + let outStockParts: Int + let weekPeriod: String +} diff --git a/SampoomManagement/Features/Dashboard/Domain/Repository/DashboardRepository.swift b/SampoomManagement/Features/Dashboard/Domain/Repository/DashboardRepository.swift new file mode 100644 index 0000000..d9c3ef6 --- /dev/null +++ b/SampoomManagement/Features/Dashboard/Domain/Repository/DashboardRepository.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol DashboardRepository { + func getDashboard() async throws -> Dashboard + func getWeeklySummary() async throws -> WeeklySummary +} diff --git a/SampoomManagement/Features/Dashboard/Domain/UseCase/GetDashboardUseCase.swift b/SampoomManagement/Features/Dashboard/Domain/UseCase/GetDashboardUseCase.swift new file mode 100644 index 0000000..57aa2a1 --- /dev/null +++ b/SampoomManagement/Features/Dashboard/Domain/UseCase/GetDashboardUseCase.swift @@ -0,0 +1,13 @@ +import Foundation + +struct GetDashboardUseCase { + private let repository: DashboardRepository + + init(repository: DashboardRepository) { + self.repository = repository + } + + func execute() async throws -> Dashboard { + return try await repository.getDashboard() + } +} diff --git a/SampoomManagement/Features/Dashboard/Domain/UseCase/GetWeeklySummaryUseCase.swift b/SampoomManagement/Features/Dashboard/Domain/UseCase/GetWeeklySummaryUseCase.swift new file mode 100644 index 0000000..92b4dac --- /dev/null +++ b/SampoomManagement/Features/Dashboard/Domain/UseCase/GetWeeklySummaryUseCase.swift @@ -0,0 +1,13 @@ +import Foundation + +struct GetWeeklySummaryUseCase { + private let repository: DashboardRepository + + init(repository: DashboardRepository) { + self.repository = repository + } + + func execute() async throws -> WeeklySummary { + return try await repository.getWeeklySummary() + } +} diff --git a/SampoomManagement/Features/Dashboard/UI/DashboardUiState.swift b/SampoomManagement/Features/Dashboard/UI/DashboardUiState.swift index 3d4f100..a3bde4b 100644 --- a/SampoomManagement/Features/Dashboard/UI/DashboardUiState.swift +++ b/SampoomManagement/Features/Dashboard/UI/DashboardUiState.swift @@ -9,28 +9,48 @@ import Foundation struct DashboardUiState: Equatable { let orderList: [Order] + let dashboard: Dashboard? + let weeklySummary: WeeklySummary? let dashboardLoading: Bool let dashboardError: String? + let weeklySummaryLoading: Bool + let weeklySummaryError: String? init( orderList: [Order] = [], + dashboard: Dashboard? = nil, + weeklySummary: WeeklySummary? = nil, dashboardLoading: Bool = false, - dashboardError: String? = nil + dashboardError: String? = nil, + weeklySummaryLoading: Bool = false, + weeklySummaryError: String? = nil ) { self.orderList = orderList + self.dashboard = dashboard + self.weeklySummary = weeklySummary self.dashboardLoading = dashboardLoading self.dashboardError = dashboardError + self.weeklySummaryLoading = weeklySummaryLoading + self.weeklySummaryError = weeklySummaryError } func copy( orderList: [Order]? = nil, + dashboard: Dashboard?? = nil, + weeklySummary: WeeklySummary?? = nil, dashboardLoading: Bool? = nil, - dashboardError: String? = nil + dashboardError: String?? = nil, + weeklySummaryLoading: Bool? = nil, + weeklySummaryError: String?? = nil ) -> DashboardUiState { return DashboardUiState( orderList: orderList ?? self.orderList, + dashboard: dashboard ?? self.dashboard, + weeklySummary: weeklySummary ?? self.weeklySummary, dashboardLoading: dashboardLoading ?? self.dashboardLoading, - dashboardError: dashboardError ?? self.dashboardError + dashboardError: dashboardError ?? self.dashboardError, + weeklySummaryLoading: weeklySummaryLoading ?? self.weeklySummaryLoading, + weeklySummaryError: weeklySummaryError ?? self.weeklySummaryError ) } } diff --git a/SampoomManagement/Features/Dashboard/UI/DashboardView.swift b/SampoomManagement/Features/Dashboard/UI/DashboardView.swift index 6abda9e..d8e5ceb 100644 --- a/SampoomManagement/Features/Dashboard/UI/DashboardView.swift +++ b/SampoomManagement/Features/Dashboard/UI/DashboardView.swift @@ -45,6 +45,7 @@ struct DashboardView: View { titleSection buttonSection orderListSection + weeklySummarySection Spacer(minLength: 100) } .padding(.horizontal, 16) @@ -79,17 +80,18 @@ struct DashboardView: View { } private var buttonSection: some View { - VStack(spacing: 16) { + let dash = viewModel.uiState.dashboard + return VStack(spacing: 16) { if userRole.isAdmin { buttonCard(iconName: "employee", valueText: "45", subText: StringResources.Dashboard.employee, bordered: true) {} } HStack(spacing: 16) { - buttonCard(iconName: "parts", valueText: "1234", subText: StringResources.Dashboard.partsOnHand) {} - buttonCard(iconName: "orders", valueText: "23", subText: StringResources.Dashboard.partsInProgress) {} + buttonCard(iconName: "car", valueText: String(dash?.totalParts ?? 0), subText: StringResources.Dashboard.partsOnHand) {} + buttonCard(iconName: "block", valueText: String(dash?.outOfStockParts ?? 0), subText: StringResources.Dashboard.shortageOfParts) {} } HStack(spacing: 16) { - buttonCard(iconName: "warning", valueText: "19", subText: StringResources.Dashboard.shortageOfParts) {} - buttonCard(iconName: "money", valueText: "4,123,200", subText: StringResources.Dashboard.orderAmount) {} + buttonCard(iconName: "warning", valueText: String(dash?.lowStockParts ?? 0), subText: StringResources.Dashboard.shortageOfParts) {} + buttonCard(iconName: "parts", valueText: String(dash?.totalQuantity ?? 0), subText: StringResources.Dashboard.partsOnHand) {} } } .padding(.bottom, 16) @@ -127,6 +129,7 @@ struct DashboardView: View { HStack { Text(StringResources.Dashboard.recentOrdersTitle) .font(.gmarketTitle2) + .fontWeight(.bold) .foregroundColor(.text) Spacer() Button(action: { onNavigateOrderList() }) { @@ -136,14 +139,7 @@ struct DashboardView: View { } } - if viewModel.uiState.dashboardLoading { - HStack { Spacer(); ProgressView(); Spacer() } - .padding(.vertical, 32) - } else if let error = viewModel.uiState.dashboardError { - VStack { Text(error).foregroundColor(.red) } - .frame(maxWidth: .infinity) - .padding(.vertical, 32) - } else if viewModel.uiState.orderList.isEmpty { + if viewModel.uiState.orderList.isEmpty { VStack { Text(StringResources.Order.emptyList).foregroundColor(.textSecondary) } .frame(maxWidth: .infinity) .padding(.vertical, 32) @@ -156,6 +152,44 @@ struct DashboardView: View { } } } + + private var weeklySummarySection: some View { + let weekly = viewModel.uiState.weeklySummary + return VStack(alignment: .leading, spacing: 16) { + Text(StringResources.Dashboard.weeklySummaryTitle) + .font(.gmarketTitle2) + .fontWeight(.bold) + .foregroundColor(.text) + + HStack(spacing: 0) { + VStack(spacing: 8) { + Text(String(weekly?.inStockParts ?? 0)) + .font(.gmarketTitle2) + .fontWeight(.bold) + .foregroundColor(.green) + Text(StringResources.Dashboard.weeklySummaryInStock) + .font(.gmarketBody) + .foregroundColor(.textSecondary) + } + .frame(maxWidth: .infinity) + + VStack(spacing: 8) { + Text(String(weekly?.outStockParts ?? 0)) + .font(.gmarketTitle2) + .fontWeight(.bold) + .foregroundColor(.red) + Text(StringResources.Dashboard.weeklySummaryOutStock) + .font(.gmarketBody) + .foregroundColor(.textSecondary) + } + .frame(maxWidth: .infinity) + } + } + .frame(maxWidth: .infinity) + .padding(16) + .background(Color.backgroundCard) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } } diff --git a/SampoomManagement/Features/Dashboard/UI/DashboardViewModel.swift b/SampoomManagement/Features/Dashboard/UI/DashboardViewModel.swift index f278418..2de2522 100644 --- a/SampoomManagement/Features/Dashboard/UI/DashboardViewModel.swift +++ b/SampoomManagement/Features/Dashboard/UI/DashboardViewModel.swift @@ -14,37 +14,85 @@ class DashboardViewModel: ObservableObject { @Published var uiState = DashboardUiState() private let getOrderUseCase: GetOrderUseCase + private let getDashboardUseCase: GetDashboardUseCase + private let getWeeklySummaryUseCase: GetWeeklySummaryUseCase + private let messageHandler: GlobalMessageHandler - init(getOrderUseCase: GetOrderUseCase) { + init( + getOrderUseCase: GetOrderUseCase, + getDashboardUseCase: GetDashboardUseCase, + getWeeklySummaryUseCase: GetWeeklySummaryUseCase, + messageHandler: GlobalMessageHandler + ) { self.getOrderUseCase = getOrderUseCase - loadOrderList() + self.getDashboardUseCase = getDashboardUseCase + self.getWeeklySummaryUseCase = getWeeklySummaryUseCase + self.messageHandler = messageHandler + loadAll() } func onEvent(_ event: DashboardUiEvent) { switch event { case .loadDashboard, .retryDashboard: - loadOrderList() + loadAll() } } + private func loadAll() { + loadOrderList() + Task { await loadDashboard() } + Task { await loadWeeklySummary() } + } + private func loadOrderList() { Task { - uiState = uiState.copy(dashboardLoading: true, dashboardError: nil) do { let orderList = try await getOrderUseCase.execute() uiState = uiState.copy( orderList: Array(orderList.items.prefix(5)), - dashboardLoading: false, - dashboardError: nil + dashboardLoading: false ) } catch { - uiState = uiState.copy( - dashboardLoading: false, - dashboardError: error.localizedDescription - ) + messageHandler.showMessage(error.localizedDescription, isError: true) } } } + + private func loadDashboard() async { + uiState = uiState.copy(dashboardLoading: true, dashboardError: .some(nil)) + do { + let dashboard = try await getDashboardUseCase.execute() + uiState = uiState.copy( + dashboard: dashboard, + dashboardLoading: false, + dashboardError: .some(nil) + ) + } catch { + messageHandler.showMessage(error.localizedDescription, isError: true) + uiState = uiState.copy( + dashboardLoading: false, + dashboardError: .some(error.localizedDescription) + ) + } + } + + private func loadWeeklySummary() async { + uiState = uiState.copy(weeklySummaryLoading: true, weeklySummaryError: .some(nil)) + do { + let weekly = try await getWeeklySummaryUseCase.execute() + uiState = uiState.copy( + weeklySummary: weekly, + weeklySummaryLoading: false, + weeklySummaryError: .some(nil) + ) + } catch { + messageHandler.showMessage(error.localizedDescription, isError: true) + uiState = uiState.copy( + weeklySummaryLoading: false, + weeklySummaryError: .some(error.localizedDescription) + ) + } + } } diff --git a/SampoomManagement/Resources/Assets.xcassets/block.imageset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/block.imageset/Contents.json new file mode 100644 index 0000000..9fbef6a --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/block.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "block.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Resources/Assets.xcassets/block.imageset/block.svg b/SampoomManagement/Resources/Assets.xcassets/block.imageset/block.svg new file mode 100644 index 0000000..6ff1c81 --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/block.imageset/block.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/SampoomManagement/Resources/Assets.xcassets/car.imageset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/car.imageset/Contents.json new file mode 100644 index 0000000..d059f1a --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/car.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "car.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Resources/Assets.xcassets/car.imageset/car.svg b/SampoomManagement/Resources/Assets.xcassets/car.imageset/car.svg new file mode 100644 index 0000000..9ac9add --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/car.imageset/car.svg @@ -0,0 +1 @@ + \ No newline at end of file