From ac61bcfb9ea10a967e27e9ac27777eb9f9b53a64 Mon Sep 17 00:00:00 2001 From: William Brawner Date: Sun, 10 Oct 2021 19:39:59 -0600 Subject: [PATCH] Add basic expected vs actual details for budgets --- Twigs/Budget/Budget.swift | 9 ++ Twigs/Budget/BudgetDetailsView.swift | 34 +++++--- Twigs/Budget/BudgetsDataStore.swift | 110 +++++++++++++++++++++++-- Twigs/Category/Category.swift | 5 ++ Twigs/Category/CategoryDataStore.swift | 2 +- Twigs/DataStoreProvider.swift | 2 +- 6 files changed, 144 insertions(+), 18 deletions(-) diff --git a/Twigs/Budget/Budget.swift b/Twigs/Budget/Budget.swift index cc14843..53bb8e8 100644 --- a/Twigs/Budget/Budget.swift +++ b/Twigs/Budget/Budget.swift @@ -14,3 +14,12 @@ struct Budget: Identifiable, Hashable, Codable { let description: String? let currencyCode: String? } + +struct BudgetOverview { + let budget: Budget + let balance: Int + var expectedIncome: Int = 0 + var actualIncome: Int = 0 + var expectedExpenses: Int = 0 + var actualExpenses: Int = 0 +} diff --git a/Twigs/Budget/BudgetDetailsView.swift b/Twigs/Budget/BudgetDetailsView.swift index c2bc2c1..5e1ec18 100644 --- a/Twigs/Budget/BudgetDetailsView.swift +++ b/Twigs/Budget/BudgetDetailsView.swift @@ -9,28 +9,40 @@ import SwiftUI struct BudgetDetailsView: View { - @EnvironmentObject var transactionDataStore: TransactionDataStore - @State var sumId: String = "" + @EnvironmentObject var budgetDataStore: BudgetsDataStore + @State var requestedOverview = "" let budget: Budget @ViewBuilder var body: some View { ScrollView { VStack { - switch transactionDataStore.sums[sumId] { + switch budgetDataStore.overview { case .failure(.loading): - ActivityIndicator(isAnimating: .constant(true), style: .large).onAppear { - if self.sumId == "" { - self.sumId = transactionDataStore.sum(budgetId: self.budget.id) - } - } - case .success(let sum): + ActivityIndicator(isAnimating: .constant(true), style: .large) + case .success(let overview): Text("Current Balance:") - Text(verbatim: sum.balance.toCurrencyString()) - .foregroundColor(sum.balance < 0 ? .red : .green) + Text(verbatim: overview.balance.toCurrencyString()) + .foregroundColor(overview.balance < 0 ? .red : .green) + Text("Expected Income:") + Text(verbatim: overview.expectedIncome.toCurrencyString()) + Text("Actual Income:") + Text(verbatim: overview.actualIncome.toCurrencyString()) + .foregroundColor(.green) + Text("Expected Expenses:") + Text(verbatim: overview.expectedExpenses.toCurrencyString()) + Text("Actual Expenses:") + Text(verbatim: overview.actualExpenses.toCurrencyString()) + .foregroundColor(.red) default: Text("An error has ocurred") } + }.onAppear { + if requestedOverview != budget.id { + print("requesting overview") + requestedOverview = budget.id + budgetDataStore.loadOverview(budget) + } } } } diff --git a/Twigs/Budget/BudgetsDataStore.swift b/Twigs/Budget/BudgetsDataStore.swift index 8bd6983..df88224 100644 --- a/Twigs/Budget/BudgetsDataStore.swift +++ b/Twigs/Budget/BudgetsDataStore.swift @@ -10,6 +10,9 @@ import Foundation import Combine class BudgetsDataStore: ObservableObject { + private let budgetRepository: BudgetRepository + private let categoryRepository: CategoryRepository + private let transactionRepository: TransactionRepository private var currentRequest: AnyCancellable? = nil @Published var budgets: Result<[Budget], NetworkError> = .failure(.loading) @Published var budget: Budget? = nil { @@ -17,6 +20,13 @@ class BudgetsDataStore: ObservableObject { UserDefaults.standard.set(budget?.id, forKey: LAST_BUDGET) } } + @Published var overview: Result = .failure(.loading) + + init(budgetRepository: BudgetRepository, categoryRepository: CategoryRepository, transactionRepository: TransactionRepository) { + self.budgetRepository = budgetRepository + self.categoryRepository = categoryRepository + self.transactionRepository = transactionRepository + } func getBudgets(count: Int? = nil, page: Int? = nil) { self.budgets = .failure(.loading) @@ -38,7 +48,6 @@ class BudgetsDataStore: ObservableObject { print("failed to load budgets: \(error.name)") } - self.budgets = .failure(error) return } @@ -51,12 +60,103 @@ class BudgetsDataStore: ObservableObject { } }) } - - init(_ budgetRepository: BudgetRepository) { - self.budgetRepository = budgetRepository + + func loadOverview(_ budget: Budget) { + self.overview = .failure(.loading) + self.currentRequest = self.transactionRepository.sumTransactions(budgetId: budget.id, categoryId: nil, from: nil, to: nil) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { (status) in + switch status { + case .finished: + return + case .failure(let error): + switch error { + case .jsonParsingFailed(let wrappedError): + if let networkError = wrappedError as? NetworkError { + print("failed to load budget overview: \(networkError.name)") + } + default: + print("failed to load budget overview: \(error.name)") + } + self.budgets = .failure(error) + self.currentRequest = nil + return + } + }, receiveValue: { (response) in + self.sumCategories(budget: budget, balance: response.balance) + }) } - private let budgetRepository: BudgetRepository + private func sumCategories(budget: Budget, balance: Int) { + self.currentRequest = self.categoryRepository.getCategories(budgetId: budget.id, expense: nil, archived: false, count: nil, page: nil) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { (status) in + switch status { + case .finished: + self.currentRequest = nil + return + case .failure(let error): + switch error { + case .jsonParsingFailed(let wrappedError): + if let networkError = wrappedError as? NetworkError { + print("failed to load budget overview: \(networkError.name)") + } + default: + print("failed to load budget overview: \(error.name)") + } + self.budgets = .failure(error) + return + } + }, receiveValue: { (categories) in + var budgetOverview = BudgetOverview(budget: budget, balance: balance) + budgetOverview.expectedIncome = 0 + budgetOverview.expectedIncome = 0 + budgetOverview.actualIncome = 0 + budgetOverview.actualIncome = 0 + var categorySums: [AnyPublisher] = [] + categories.forEach { category in + if category.expense { + budgetOverview.expectedExpenses += category.amount + } else { + budgetOverview.expectedIncome += category.amount + } + categorySums.append(self.transactionRepository.sumTransactions(budgetId: nil, categoryId: category.id, from: nil, to: nil).map { + CategoryBalance(category: category, balance: $0.balance) + }.eraseToAnyPublisher()) + } + + self.currentRequest = Publishers.MergeMany(categorySums) + .collect() + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { status in + switch status { + case .finished: + self.currentRequest = nil + return + case .failure(let error): + switch error { + case .jsonParsingFailed(let wrappedError): + if let networkError = wrappedError as? NetworkError { + print("failed to load budget overview: \(networkError.name)") + } + default: + print("failed to load budget overview: \(error.name)") + } + self.overview = .failure(error) + return + } + }, receiveValue: { + $0.forEach { categoryBalance in + if categoryBalance.category.expense { + budgetOverview.actualExpenses += abs(categoryBalance.balance) + } else { + budgetOverview.actualIncome += categoryBalance.balance + } + } + self.overview = .success(budgetOverview) + }) + }) + } } private let LAST_BUDGET = "LAST_BUDGET" diff --git a/Twigs/Category/Category.swift b/Twigs/Category/Category.swift index f9872b6..e5e80c4 100644 --- a/Twigs/Category/Category.swift +++ b/Twigs/Category/Category.swift @@ -17,3 +17,8 @@ struct Category: Identifiable, Hashable, Codable { let expense: Bool let archived: Bool } + +struct CategoryBalance { + let category: Category + let balance: Int +} diff --git a/Twigs/Category/CategoryDataStore.swift b/Twigs/Category/CategoryDataStore.swift index 0d68fc0..9eed036 100644 --- a/Twigs/Category/CategoryDataStore.swift +++ b/Twigs/Category/CategoryDataStore.swift @@ -20,7 +20,7 @@ class CategoryDataStore: ObservableObject { } func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = false, count: Int? = nil, page: Int? = nil) -> String { - let requestId = "\(budgetId ?? "all")-\(String(describing: expense))" + let requestId = "\(budgetId ?? "all")-\(String(describing: expense))-\(String(describing: archived))" self.categories[requestId] = .failure(.loading) self.currentRequest = categoryRepository.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page) diff --git a/Twigs/DataStoreProvider.swift b/Twigs/DataStoreProvider.swift index 5f53c57..45916f9 100644 --- a/Twigs/DataStoreProvider.swift +++ b/Twigs/DataStoreProvider.swift @@ -20,7 +20,7 @@ class DataStoreProvider { private let _authenticationDataStore: AuthenticationDataStore func budgetsDataStore() -> BudgetsDataStore { - return BudgetsDataStore(budgetRepository) + return BudgetsDataStore(budgetRepository: budgetRepository, categoryRepository: categoryRepository, transactionRepository: transactionRepository) } func categoryDataStore() -> CategoryDataStore {