Implement budget creation

This commit is contained in:
William Brawner 2022-05-17 21:13:48 -06:00
parent e23b6db257
commit aaa5c08fb5
7 changed files with 172 additions and 13 deletions

View file

@ -49,6 +49,7 @@
8076A8522809FE99006B9DC9 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A8512809FE99006B9DC9 /* Collections */; }; 8076A8522809FE99006B9DC9 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A8512809FE99006B9DC9 /* Collections */; };
80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80820144275FFD380040996E /* SidebarBudgetView.swift */; }; 80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80820144275FFD380040996E /* SidebarBudgetView.swift */; };
808CA1A728354005002EDD59 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 808CA1A628354005002EDD59 /* XCTest.framework */; }; 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 */; }; 809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */; };
80A419ED2787C0A00090C515 /* TwigsCli.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A419EC2787C0A00090C515 /* TwigsCli.swift */; }; 80A419ED2787C0A00090C515 /* TwigsCli.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A419EC2787C0A00090C515 /* TwigsCli.swift */; };
80A419F52787C1520090C515 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 80A419F42787C1520090C515 /* ArgumentParser */; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; 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; }; 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 */, 2841022F2342D97300EAFA29 /* BudgetListsView.swift */,
28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */, 28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */,
282126BA235CDD3C00072D52 /* BudgetDetailsView.swift */, 282126BA235CDD3C00072D52 /* BudgetDetailsView.swift */,
808CA1A828355B30002EDD59 /* BudgetFormView.swift */,
); );
path = Budget; path = Budget;
sourceTree = "<group>"; sourceTree = "<group>";
@ -566,6 +569,7 @@
284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */, 284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */,
282126BD235CDE1400072D52 /* ProgressView.swift in Sources */, 282126BD235CDE1400072D52 /* ProgressView.swift in Sources */,
806C7850272B700B00FA1375 /* TwigsApp.swift in Sources */, 806C7850272B700B00FA1375 /* TwigsApp.swift in Sources */,
808CA1A928355B30002EDD59 /* BudgetFormView.swift in Sources */,
28CE8B9523525F990072BC4C /* Extensions.swift in Sources */, 28CE8B9523525F990072BC4C /* Extensions.swift in Sources */,
801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */, 801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */,
); );

View 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()
}
}

View file

@ -20,14 +20,12 @@ struct BudgetListsView: View {
action: { await self.dataStore.getBudgets(count: nil, page: nil) }, action: { await self.dataStore.getBudgets(count: nil, page: nil) },
errorTextLocalizedStringKey: "budgets_load_failure" errorTextLocalizedStringKey: "budgets_load_failure"
) { (budgets: [Budget]) in ) { (budgets: [Budget]) in
Section("budgets") {
ForEach(budgets) { budget in ForEach(budgets) { budget in
BudgetListItemView(budget) BudgetListItemView(budget)
} }
} }
} }
} }
}
struct BudgetListItemView: View { struct BudgetListItemView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore

View file

@ -27,6 +27,7 @@ class DataStore : ObservableObject {
} }
@Published var overview: AsyncData<BudgetOverview> = .empty @Published var overview: AsyncData<BudgetOverview> = .empty
@Published var showBudgetSelection: Bool = true @Published var showBudgetSelection: Bool = true
@Published var editingBudget: Bool = false
init( init(
_ apiService: TwigsApiService _ 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 { func loadOverview(_ budget: Budget) async {
self.overview = .loading self.overview = .loading
do { 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) self.budget = .success(budget)
await loadOverview(budget) await loadOverview(budget)
await getTransactions() await getTransactions()
await getCategories(budgetId: budget.id, expense: nil, archived: nil, count: nil, page: nil) await getCategories(budgetId: budget.id, expense: nil, archived: nil, count: nil, page: nil)
await getRecurringTransactions() await getRecurringTransactions()
} else {
self.budget = .empty
self.transactions = .empty
self.categories = .empty
self.recurringTransactions = .empty
}
} }
@Published var categories: AsyncData<[TwigsCore.Category]> = .empty @Published var categories: AsyncData<[TwigsCore.Category]> = .empty

View file

@ -81,9 +81,23 @@ struct TabbedBudgetView: View {
.sheet(isPresented: $dataStore.showBudgetSelection, .sheet(isPresented: $dataStore.showBudgetSelection,
content: { content: {
NavigationView {
VStack {
List { List {
BudgetListsView().environmentObject(dataStore) 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) .interactiveDismissDisabled(true)
}) })
} }

View file

@ -64,6 +64,7 @@
// MARK: Budgets // MARK: Budgets
"budget" = "Budget"; "budget" = "Budget";
"budgets" = "Budgets"; "budgets" = "Budgets";
"new_budget" = "New Budget";
"current_balance" = "Current Balance:"; "current_balance" = "Current Balance:";
"expected" = "Expected:"; "expected" = "Expected:";
"actual" = "Actual:"; "actual" = "Actual:";

View file

@ -64,6 +64,7 @@
// MARK: Budgets // MARK: Budgets
"budget" = "Presupuesto"; "budget" = "Presupuesto";
"budgets" = "Presupuestos"; "budgets" = "Presupuestos";
"new_budget" = "Nuevo Presupuesto";
"current_balance" = "Saldo Actual:"; "current_balance" = "Saldo Actual:";
"income" = "Ingresos:"; "income" = "Ingresos:";
"expenses" = "Gastos:"; "expenses" = "Gastos:";