Add basic expected vs actual details for budgets

This commit is contained in:
William Brawner 2021-10-10 19:39:59 -06:00
parent 7b99811cd9
commit ac61bcfb9e
6 changed files with 144 additions and 18 deletions

View file

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

View file

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

View file

@ -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<BudgetOverview, NetworkError> = .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<CategoryBalance, NetworkError>] = []
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"

View file

@ -17,3 +17,8 @@ struct Category: Identifiable, Hashable, Codable {
let expense: Bool
let archived: Bool
}
struct CategoryBalance {
let category: Category
let balance: Int
}

View file

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

View file

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