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 description: String?
let currencyCode: 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 import SwiftUI
struct BudgetDetailsView: View { struct BudgetDetailsView: View {
@EnvironmentObject var transactionDataStore: TransactionDataStore @EnvironmentObject var budgetDataStore: BudgetsDataStore
@State var sumId: String = "" @State var requestedOverview = ""
let budget: Budget let budget: Budget
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack { VStack {
switch transactionDataStore.sums[sumId] { switch budgetDataStore.overview {
case .failure(.loading): case .failure(.loading):
ActivityIndicator(isAnimating: .constant(true), style: .large).onAppear { ActivityIndicator(isAnimating: .constant(true), style: .large)
if self.sumId == "" { case .success(let overview):
self.sumId = transactionDataStore.sum(budgetId: self.budget.id)
}
}
case .success(let sum):
Text("Current Balance:") Text("Current Balance:")
Text(verbatim: sum.balance.toCurrencyString()) Text(verbatim: overview.balance.toCurrencyString())
.foregroundColor(sum.balance < 0 ? .red : .green) .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: default:
Text("An error has ocurred") 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 import Combine
class BudgetsDataStore: ObservableObject { class BudgetsDataStore: ObservableObject {
private let budgetRepository: BudgetRepository
private let categoryRepository: CategoryRepository
private let transactionRepository: TransactionRepository
private var currentRequest: AnyCancellable? = nil private var currentRequest: AnyCancellable? = nil
@Published var budgets: Result<[Budget], NetworkError> = .failure(.loading) @Published var budgets: Result<[Budget], NetworkError> = .failure(.loading)
@Published var budget: Budget? = nil { @Published var budget: Budget? = nil {
@ -17,6 +20,13 @@ class BudgetsDataStore: ObservableObject {
UserDefaults.standard.set(budget?.id, forKey: LAST_BUDGET) 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) { func getBudgets(count: Int? = nil, page: Int? = nil) {
self.budgets = .failure(.loading) self.budgets = .failure(.loading)
@ -38,7 +48,6 @@ class BudgetsDataStore: ObservableObject {
print("failed to load budgets: \(error.name)") print("failed to load budgets: \(error.name)")
} }
self.budgets = .failure(error) self.budgets = .failure(error)
return return
} }
@ -52,11 +61,102 @@ class BudgetsDataStore: ObservableObject {
}) })
} }
init(_ budgetRepository: BudgetRepository) { func loadOverview(_ budget: Budget) {
self.budgetRepository = budgetRepository 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" private let LAST_BUDGET = "LAST_BUDGET"

View file

@ -17,3 +17,8 @@ struct Category: Identifiable, Hashable, Codable {
let expense: Bool let expense: Bool
let archived: 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 { 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.categories[requestId] = .failure(.loading)
self.currentRequest = categoryRepository.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page) 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 private let _authenticationDataStore: AuthenticationDataStore
func budgetsDataStore() -> BudgetsDataStore { func budgetsDataStore() -> BudgetsDataStore {
return BudgetsDataStore(budgetRepository) return BudgetsDataStore(budgetRepository: budgetRepository, categoryRepository: categoryRepository, transactionRepository: transactionRepository)
} }
func categoryDataStore() -> CategoryDataStore { func categoryDataStore() -> CategoryDataStore {