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 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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue