Add basic expected vs actual details for budgets
This commit is contained in:
parent
7b99811cd9
commit
ac61bcfb9e
6 changed files with 144 additions and 18 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -17,3 +17,8 @@ struct Category: Identifiable, Hashable, Codable {
|
|||
let expense: Bool
|
||||
let archived: Bool
|
||||
}
|
||||
|
||||
struct CategoryBalance {
|
||||
let category: Category
|
||||
let balance: Int
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue