From aaa5c08fb559708987a562b9bde3a131f9775f56 Mon Sep 17 00:00:00 2001 From: Billy Brawner Date: Tue, 17 May 2022 21:13:48 -0600 Subject: [PATCH] Implement budget creation --- Twigs.xcodeproj/project.pbxproj | 4 ++ Twigs/Budget/BudgetFormView.swift | 77 +++++++++++++++++++++++++++++ Twigs/Budget/BudgetListsView.swift | 6 +-- Twigs/DataStore.swift | 78 +++++++++++++++++++++++++++--- Twigs/TabbedBudgetView.swift | 18 ++++++- Twigs/en.lproj/Localizable.strings | 1 + Twigs/es.lproj/Localizable.strings | 1 + 7 files changed, 172 insertions(+), 13 deletions(-) create mode 100644 Twigs/Budget/BudgetFormView.swift diff --git a/Twigs.xcodeproj/project.pbxproj b/Twigs.xcodeproj/project.pbxproj index 36d4782..543ae33 100644 --- a/Twigs.xcodeproj/project.pbxproj +++ b/Twigs.xcodeproj/project.pbxproj @@ -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 = ""; }; 80820144275FFD380040996E /* SidebarBudgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarBudgetView.swift; sourceTree = ""; }; 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 = ""; }; 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryFormSheet.swift; sourceTree = ""; }; 809B94242722597800B1DAE2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 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 = ""; @@ -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 */, ); diff --git a/Twigs/Budget/BudgetFormView.swift b/Twigs/Budget/BudgetFormView.swift new file mode 100644 index 0000000..0365a6c --- /dev/null +++ b/Twigs/Budget/BudgetFormView.swift @@ -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() + } +} diff --git a/Twigs/Budget/BudgetListsView.swift b/Twigs/Budget/BudgetListsView.swift index 6e864b1..6a2a262 100644 --- a/Twigs/Budget/BudgetListsView.swift +++ b/Twigs/Budget/BudgetListsView.swift @@ -20,10 +20,8 @@ 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) - } + ForEach(budgets) { budget in + BudgetListItemView(budget) } } } diff --git a/Twigs/DataStore.swift b/Twigs/DataStore.swift index 490b7a7..d198031 100644 --- a/Twigs/DataStore.swift +++ b/Twigs/DataStore.swift @@ -27,7 +27,8 @@ class DataStore : ObservableObject { } @Published var overview: AsyncData = .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 { - self.budget = .success(budget) - await loadOverview(budget) - await getTransactions() - await getCategories(budgetId: budget.id, expense: nil, archived: nil, count: nil, page: nil) - await getRecurringTransactions() + 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 diff --git a/Twigs/TabbedBudgetView.swift b/Twigs/TabbedBudgetView.swift index 516e0a8..d11d271 100644 --- a/Twigs/TabbedBudgetView.swift +++ b/Twigs/TabbedBudgetView.swift @@ -81,8 +81,22 @@ struct TabbedBudgetView: View { .sheet(isPresented: $dataStore.showBudgetSelection, content: { - List { - BudgetListsView().environmentObject(dataStore) + 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) }) diff --git a/Twigs/en.lproj/Localizable.strings b/Twigs/en.lproj/Localizable.strings index da6ff77..ceb8725 100644 --- a/Twigs/en.lproj/Localizable.strings +++ b/Twigs/en.lproj/Localizable.strings @@ -64,6 +64,7 @@ // MARK: Budgets "budget" = "Budget"; "budgets" = "Budgets"; +"new_budget" = "New Budget"; "current_balance" = "Current Balance:"; "expected" = "Expected:"; "actual" = "Actual:"; diff --git a/Twigs/es.lproj/Localizable.strings b/Twigs/es.lproj/Localizable.strings index 51591f3..f92be66 100644 --- a/Twigs/es.lproj/Localizable.strings +++ b/Twigs/es.lproj/Localizable.strings @@ -64,6 +64,7 @@ // MARK: Budgets "budget" = "Presupuesto"; "budgets" = "Presupuestos"; +"new_budget" = "Nuevo Presupuesto"; "current_balance" = "Saldo Actual:"; "income" = "Ingresos:"; "expenses" = "Gastos:";