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 */; };
|
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 */,
|
||||||
);
|
);
|
||||||
|
|
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) },
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:";
|
||||||
|
|
|
@ -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:";
|
||||||
|
|
Loading…
Reference in a new issue