Implement budget creation
This commit is contained in:
parent
e23b6db257
commit
aaa5c08fb5
7 changed files with 172 additions and 13 deletions
|
@ -49,6 +49,7 @@
|
|||
8076A8522809FE99006B9DC9 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A8512809FE99006B9DC9 /* Collections */; };
|
||||
80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80820144275FFD380040996E /* SidebarBudgetView.swift */; };
|
||||
808CA1A728354005002EDD59 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 808CA1A628354005002EDD59 /* XCTest.framework */; };
|
||||
808CA1A928355B30002EDD59 /* BudgetFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 808CA1A828355B30002EDD59 /* BudgetFormView.swift */; };
|
||||
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */; };
|
||||
80A419ED2787C0A00090C515 /* TwigsCli.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A419EC2787C0A00090C515 /* TwigsCli.swift */; };
|
||||
80A419F52787C1520090C515 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 80A419F42787C1520090C515 /* ArgumentParser */; };
|
||||
|
@ -133,6 +134,7 @@
|
|||
806C784F272B700B00FA1375 /* TwigsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsApp.swift; sourceTree = "<group>"; };
|
||||
80820144275FFD380040996E /* SidebarBudgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarBudgetView.swift; sourceTree = "<group>"; };
|
||||
808CA1A628354005002EDD59 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
|
||||
808CA1A828355B30002EDD59 /* BudgetFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetFormView.swift; sourceTree = "<group>"; };
|
||||
809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryFormSheet.swift; sourceTree = "<group>"; };
|
||||
809B94242722597800B1DAE2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
80A419EA2787C0A00090C515 /* twigs-cli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "twigs-cli"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -192,6 +194,7 @@
|
|||
2841022F2342D97300EAFA29 /* BudgetListsView.swift */,
|
||||
28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */,
|
||||
282126BA235CDD3C00072D52 /* BudgetDetailsView.swift */,
|
||||
808CA1A828355B30002EDD59 /* BudgetFormView.swift */,
|
||||
);
|
||||
path = Budget;
|
||||
sourceTree = "<group>";
|
||||
|
@ -566,6 +569,7 @@
|
|||
284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */,
|
||||
282126BD235CDE1400072D52 /* ProgressView.swift in Sources */,
|
||||
806C7850272B700B00FA1375 /* TwigsApp.swift in Sources */,
|
||||
808CA1A928355B30002EDD59 /* BudgetFormView.swift in Sources */,
|
||||
28CE8B9523525F990072BC4C /* Extensions.swift in Sources */,
|
||||
801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */,
|
||||
);
|
||||
|
|
77
Twigs/Budget/BudgetFormView.swift
Normal file
77
Twigs/Budget/BudgetFormView.swift
Normal file
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// BudgetFormView.swift
|
||||
// Twigs
|
||||
//
|
||||
// Created by William Brawner on 5/18/22.
|
||||
// Copyright © 2022 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import TwigsCore
|
||||
|
||||
struct BudgetFormView: View {
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
@State private var title = ""
|
||||
@State private var description = ""
|
||||
@State private var showingAlert = false
|
||||
private var budgetId: String {
|
||||
get {
|
||||
if case let .editing(budget) = dataStore.budget {
|
||||
return budget.id
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var stateContent: some View {
|
||||
switch dataStore.category {
|
||||
case .success(_):
|
||||
EmptyView()
|
||||
case .saving(_):
|
||||
EmbeddedLoadingView()
|
||||
default:
|
||||
Form {
|
||||
TextField("prompt_name", text: $title)
|
||||
.textInputAutocapitalization(.words)
|
||||
TextField("prompt_description", text: $description)
|
||||
.textInputAutocapitalization(.sentences )
|
||||
if case let .editing(budget) = dataStore.budget, budget.id != "" {
|
||||
Button(action: {
|
||||
self.showingAlert = true
|
||||
}) {
|
||||
Text("delete")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.alert(isPresented: $showingAlert) {
|
||||
Alert(title: Text("confirm_delete"), message: Text("cannot_undo"), primaryButton: .destructive(Text("delete"), action: {
|
||||
Task {
|
||||
await self.dataStore.deleteBudget()
|
||||
}
|
||||
}), secondaryButton: .cancel())
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
stateContent
|
||||
.navigationBarItems(
|
||||
trailing: Button("save") {
|
||||
Task {
|
||||
await self.dataStore.save(Budget(id: budgetId, name: title, description: description, currencyCode: nil))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct BudgetFormView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BudgetFormView()
|
||||
}
|
||||
}
|
|
@ -20,14 +20,12 @@ struct BudgetListsView: View {
|
|||
action: { await self.dataStore.getBudgets(count: nil, page: nil) },
|
||||
errorTextLocalizedStringKey: "budgets_load_failure"
|
||||
) { (budgets: [Budget]) in
|
||||
Section("budgets") {
|
||||
ForEach(budgets) { budget in
|
||||
BudgetListItemView(budget)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BudgetListItemView: View {
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
|
|
|
@ -27,6 +27,7 @@ class DataStore : ObservableObject {
|
|||
}
|
||||
@Published var overview: AsyncData<BudgetOverview> = .empty
|
||||
@Published var showBudgetSelection: Bool = true
|
||||
@Published var editingBudget: Bool = false
|
||||
|
||||
init(
|
||||
_ apiService: TwigsApiService
|
||||
|
@ -59,6 +60,61 @@ class DataStore : ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func newBudget() {
|
||||
self.budget = .editing(Budget(id: "", name: "", description: "", currencyCode: ""))
|
||||
self.editingBudget = true
|
||||
}
|
||||
|
||||
func save(_ budget: Budget) async {
|
||||
self.budget = .saving(budget)
|
||||
do {
|
||||
var savedBudget: Budget
|
||||
if budget.id != "" {
|
||||
savedBudget = try await self.apiService.updateBudget(budget)
|
||||
} else {
|
||||
savedBudget = try await self.apiService.newBudget(budget)
|
||||
}
|
||||
await self.selectBudget(savedBudget)
|
||||
if case let .success(budgets) = self.budgets {
|
||||
var updatedBudgets = budgets.filter(withoutId: budget.id)
|
||||
updatedBudgets.append(savedBudget)
|
||||
self.budgets = .success(updatedBudgets.sorted(by: { $0.name < $1.name }))
|
||||
}
|
||||
} catch {
|
||||
self.budget = .error(error, budget)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteBudget() async {
|
||||
guard case let .editing(budget) = self.budget, budget.id != "" else {
|
||||
return
|
||||
}
|
||||
self.budget = .loading
|
||||
do {
|
||||
try await self.apiService.deleteBudget(budget.id)
|
||||
await self.selectBudget(nil)
|
||||
if case let .success(budgets) = self.budgets {
|
||||
self.budgets = .success(budgets.filter(withoutId: budget.id))
|
||||
}
|
||||
} catch {
|
||||
self.budget = .error(error, budget)
|
||||
}
|
||||
}
|
||||
|
||||
func cancelEditBudget() async {
|
||||
guard case let .success(budgets) = self.budgets else {
|
||||
return
|
||||
}
|
||||
if budgets.isEmpty {
|
||||
// Prevent the user from exiting the new budget flow if they haven't created any budgets yet
|
||||
return
|
||||
}
|
||||
guard case let .editing(budget) = self.budget else {
|
||||
return
|
||||
}
|
||||
await self.selectBudget(budget)
|
||||
}
|
||||
|
||||
func loadOverview(_ budget: Budget) async {
|
||||
self.overview = .loading
|
||||
do {
|
||||
|
@ -92,12 +148,20 @@ class DataStore : ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func selectBudget(_ budget: Budget) async {
|
||||
func selectBudget(_ budget: Budget?) async {
|
||||
self.editingBudget = false
|
||||
if let budget = budget {
|
||||
self.budget = .success(budget)
|
||||
await loadOverview(budget)
|
||||
await getTransactions()
|
||||
await getCategories(budgetId: budget.id, expense: nil, archived: nil, count: nil, page: nil)
|
||||
await getRecurringTransactions()
|
||||
} else {
|
||||
self.budget = .empty
|
||||
self.transactions = .empty
|
||||
self.categories = .empty
|
||||
self.recurringTransactions = .empty
|
||||
}
|
||||
}
|
||||
|
||||
@Published var categories: AsyncData<[TwigsCore.Category]> = .empty
|
||||
|
|
|
@ -81,9 +81,23 @@ struct TabbedBudgetView: View {
|
|||
|
||||
.sheet(isPresented: $dataStore.showBudgetSelection,
|
||||
content: {
|
||||
NavigationView {
|
||||
VStack {
|
||||
List {
|
||||
BudgetListsView().environmentObject(dataStore)
|
||||
}
|
||||
.navigationTitle("budgets")
|
||||
.navigationBarItems(trailing: Button(action: {dataStore.newBudget()}, label: {
|
||||
Image(systemName: "plus")
|
||||
.padding()
|
||||
}))
|
||||
NavigationLink(
|
||||
isActive: self.$dataStore.editingBudget,
|
||||
destination: { BudgetFormView().navigationTitle("new_budget") },
|
||||
label: { EmptyView() }
|
||||
)
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled(true)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
// MARK: Budgets
|
||||
"budget" = "Budget";
|
||||
"budgets" = "Budgets";
|
||||
"new_budget" = "New Budget";
|
||||
"current_balance" = "Current Balance:";
|
||||
"expected" = "Expected:";
|
||||
"actual" = "Actual:";
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
// MARK: Budgets
|
||||
"budget" = "Presupuesto";
|
||||
"budgets" = "Presupuestos";
|
||||
"new_budget" = "Nuevo Presupuesto";
|
||||
"current_balance" = "Saldo Actual:";
|
||||
"income" = "Ingresos:";
|
||||
"expenses" = "Gastos:";
|
||||
|
|
Loading…
Reference in a new issue