Implement creation/editing of recurring transactions
This commit is contained in:
parent
94272aa7f0
commit
e621f659c8
14 changed files with 905 additions and 22 deletions
|
@ -47,14 +47,20 @@
|
||||||
806C7850272B700B00FA1375 /* TwigsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806C784F272B700B00FA1375 /* TwigsApp.swift */; };
|
806C7850272B700B00FA1375 /* TwigsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806C784F272B700B00FA1375 /* TwigsApp.swift */; };
|
||||||
8076A84F2809FE8E006B9DC9 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A84E2809FE8E006B9DC9 /* ArgumentParser */; };
|
8076A84F2809FE8E006B9DC9 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A84E2809FE8E006B9DC9 /* ArgumentParser */; };
|
||||||
8076A8522809FE99006B9DC9 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A8512809FE99006B9DC9 /* Collections */; };
|
8076A8522809FE99006B9DC9 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A8512809FE99006B9DC9 /* Collections */; };
|
||||||
|
807FEAB52837F71200D05338 /* RecurringTransactionForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807FEAB42837F71200D05338 /* RecurringTransactionForm.swift */; };
|
||||||
|
807FEAB72838042500D05338 /* MultiPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807FEAB62838042500D05338 /* MultiPicker.swift */; };
|
||||||
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 */; };
|
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 */; };
|
||||||
|
80AF7A982835ED3B009565C6 /* RecurringTransactionFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80AF7A972835ED3B009565C6 /* RecurringTransactionFormView.swift */; };
|
||||||
80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */; };
|
80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */; };
|
||||||
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D2CE192833448500EDD6C2 /* DataStore.swift */; };
|
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D2CE192833448500EDD6C2 /* DataStore.swift */; };
|
||||||
|
80FC1BBE28411DD800682F21 /* YearlyFrequencyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80FC1BBD28411DD800682F21 /* YearlyFrequencyPicker.swift */; };
|
||||||
|
80FC1BC0284146A000682F21 /* WeeklyFrequencyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80FC1BBF284146A000682F21 /* WeeklyFrequencyPicker.swift */; };
|
||||||
|
80FC1BC2284146CD00682F21 /* MonthlyFrequencyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80FC1BC1284146CD00682F21 /* MonthlyFrequencyPicker.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -132,6 +138,8 @@
|
||||||
8044BA3C2784CC0D009A78D4 /* TransactionForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionForm.swift; sourceTree = "<group>"; };
|
8044BA3C2784CC0D009A78D4 /* TransactionForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionForm.swift; sourceTree = "<group>"; };
|
||||||
8044BA3E27853054009A78D4 /* CategoryForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryForm.swift; sourceTree = "<group>"; };
|
8044BA3E27853054009A78D4 /* CategoryForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryForm.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
|
807FEAB42837F71200D05338 /* RecurringTransactionForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionForm.swift; sourceTree = "<group>"; };
|
||||||
|
807FEAB62838042500D05338 /* MultiPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiPicker.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>"; };
|
808CA1A828355B30002EDD59 /* BudgetFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetFormView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -139,8 +147,12 @@
|
||||||
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; };
|
||||||
80A419EC2787C0A00090C515 /* TwigsCli.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsCli.swift; sourceTree = "<group>"; };
|
80A419EC2787C0A00090C515 /* TwigsCli.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsCli.swift; sourceTree = "<group>"; };
|
||||||
|
80AF7A972835ED3B009565C6 /* RecurringTransactionFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionFormView.swift; sourceTree = "<group>"; };
|
||||||
80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineLoadingView.swift; sourceTree = "<group>"; };
|
80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineLoadingView.swift; sourceTree = "<group>"; };
|
||||||
80D2CE192833448500EDD6C2 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
|
80D2CE192833448500EDD6C2 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
|
||||||
|
80FC1BBD28411DD800682F21 /* YearlyFrequencyPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YearlyFrequencyPicker.swift; sourceTree = "<group>"; };
|
||||||
|
80FC1BBF284146A000682F21 /* WeeklyFrequencyPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeeklyFrequencyPicker.swift; sourceTree = "<group>"; };
|
||||||
|
80FC1BC1284146CD00682F21 /* MonthlyFrequencyPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthlyFrequencyPicker.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -219,6 +231,10 @@
|
||||||
282126BC235CDE1400072D52 /* ProgressView.swift */,
|
282126BC235CDE1400072D52 /* ProgressView.swift */,
|
||||||
80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */,
|
80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */,
|
||||||
8005FD5C277EAB0200E48B23 /* MainView.swift */,
|
8005FD5C277EAB0200E48B23 /* MainView.swift */,
|
||||||
|
807FEAB62838042500D05338 /* MultiPicker.swift */,
|
||||||
|
80FC1BBD28411DD800682F21 /* YearlyFrequencyPicker.swift */,
|
||||||
|
80FC1BBF284146A000682F21 /* WeeklyFrequencyPicker.swift */,
|
||||||
|
80FC1BC1284146CD00682F21 /* MonthlyFrequencyPicker.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -355,6 +371,8 @@
|
||||||
801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */,
|
801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */,
|
||||||
801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */,
|
801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */,
|
||||||
801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */,
|
801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */,
|
||||||
|
80AF7A972835ED3B009565C6 /* RecurringTransactionFormView.swift */,
|
||||||
|
807FEAB42837F71200D05338 /* RecurringTransactionForm.swift */,
|
||||||
);
|
);
|
||||||
path = "Recurring Transactions";
|
path = "Recurring Transactions";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -538,6 +556,7 @@
|
||||||
files = (
|
files = (
|
||||||
2821266023555FD300072D52 /* TransactionFormSheet.swift in Sources */,
|
2821266023555FD300072D52 /* TransactionFormSheet.swift in Sources */,
|
||||||
8044BA3D2784CC0D009A78D4 /* TransactionForm.swift in Sources */,
|
8044BA3D2784CC0D009A78D4 /* TransactionForm.swift in Sources */,
|
||||||
|
80FC1BC0284146A000682F21 /* WeeklyFrequencyPicker.swift in Sources */,
|
||||||
28FE6AFC23441E4500D5543E /* CategoryRepository.swift in Sources */,
|
28FE6AFC23441E4500D5543E /* CategoryRepository.swift in Sources */,
|
||||||
801D08CE275F189E00931465 /* RecurringTransactionsRepository.swift in Sources */,
|
801D08CE275F189E00931465 /* RecurringTransactionsRepository.swift in Sources */,
|
||||||
2841022723419A2B00EAFA29 /* TabbedBudgetView.swift in Sources */,
|
2841022723419A2B00EAFA29 /* TabbedBudgetView.swift in Sources */,
|
||||||
|
@ -547,6 +566,7 @@
|
||||||
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */,
|
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */,
|
||||||
28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */,
|
28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */,
|
||||||
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */,
|
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */,
|
||||||
|
80FC1BC2284146CD00682F21 /* MonthlyFrequencyPicker.swift in Sources */,
|
||||||
802161D0277647920075761A /* AsyncObservableObject.swift in Sources */,
|
802161D0277647920075761A /* AsyncObservableObject.swift in Sources */,
|
||||||
282126BB235CDD3C00072D52 /* BudgetDetailsView.swift in Sources */,
|
282126BB235CDD3C00072D52 /* BudgetDetailsView.swift in Sources */,
|
||||||
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */,
|
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */,
|
||||||
|
@ -562,14 +582,18 @@
|
||||||
282126A1235929B800072D52 /* ProfileView.swift in Sources */,
|
282126A1235929B800072D52 /* ProfileView.swift in Sources */,
|
||||||
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */,
|
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */,
|
||||||
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */,
|
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */,
|
||||||
|
807FEAB72838042500D05338 /* MultiPicker.swift in Sources */,
|
||||||
28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */,
|
28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */,
|
||||||
|
80FC1BBE28411DD800682F21 /* YearlyFrequencyPicker.swift in Sources */,
|
||||||
8044BA3F27853054009A78D4 /* CategoryForm.swift in Sources */,
|
8044BA3F27853054009A78D4 /* CategoryForm.swift in Sources */,
|
||||||
|
807FEAB52837F71200D05338 /* RecurringTransactionForm.swift in Sources */,
|
||||||
80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */,
|
80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */,
|
||||||
8044BA3927828E9D009A78D4 /* CategoryDataStore.swift in Sources */,
|
8044BA3927828E9D009A78D4 /* CategoryDataStore.swift in Sources */,
|
||||||
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 */,
|
808CA1A928355B30002EDD59 /* BudgetFormView.swift in Sources */,
|
||||||
|
80AF7A982835ED3B009565C6 /* RecurringTransactionFormView.swift in Sources */,
|
||||||
28CE8B9523525F990072BC4C /* Extensions.swift in Sources */,
|
28CE8B9523525F990072BC4C /* Extensions.swift in Sources */,
|
||||||
801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */,
|
801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|
|
@ -33,6 +33,37 @@ class DataStore : ObservableObject {
|
||||||
@Published var showBudgetSelection: Bool = true
|
@Published var showBudgetSelection: Bool = true
|
||||||
@Published var editingBudget: Bool = false
|
@Published var editingBudget: Bool = false
|
||||||
@Published var editingCategory: Bool = false
|
@Published var editingCategory: Bool = false
|
||||||
|
@Published var editingRecurringTransaction: Bool = false
|
||||||
|
|
||||||
|
var currentUserId: String? {
|
||||||
|
get {
|
||||||
|
if case let .success(currentUser) = self.currentUser {
|
||||||
|
return currentUser.id
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var budgetId: String? {
|
||||||
|
get {
|
||||||
|
if case let .success(budget) = self.budget {
|
||||||
|
return budget.id
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoryId: String? {
|
||||||
|
get {
|
||||||
|
if case let .success(category) = self.category {
|
||||||
|
return category.id
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
_ apiService: TwigsApiService
|
_ apiService: TwigsApiService
|
||||||
|
@ -261,8 +292,12 @@ class DataStore : ObservableObject {
|
||||||
didSet {
|
didSet {
|
||||||
if case let .success(transaction) = self.recurringTransaction {
|
if case let .success(transaction) = self.recurringTransaction {
|
||||||
self.selectedRecurringTransaction = transaction
|
self.selectedRecurringTransaction = transaction
|
||||||
|
self.editingRecurringTransaction = false
|
||||||
} else if case .empty = recurringTransaction {
|
} else if case .empty = recurringTransaction {
|
||||||
self.selectedRecurringTransaction = nil
|
self.selectedRecurringTransaction = nil
|
||||||
|
self.editingRecurringTransaction = false
|
||||||
|
} else if case .editing(_) = self.recurringTransaction {
|
||||||
|
self.editingRecurringTransaction = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,6 +316,31 @@ class DataStore : ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newRecurringTransaction() {
|
||||||
|
guard case let .success(user) = self.currentUser else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard case let .success(budget) = self.budget else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.recurringTransaction = .editing(RecurringTransaction(createdBy: user.id, budgetId: budget.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func edit(_ transaction: RecurringTransaction) async {
|
||||||
|
self.recurringTransaction = .editing(transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelEditRecurringTransaction() {
|
||||||
|
guard case let .editing(rt) = self.recurringTransaction else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !rt.id.isEmpty {
|
||||||
|
self.recurringTransaction = .success(rt)
|
||||||
|
} else {
|
||||||
|
self.recurringTransaction = .empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func saveRecurringTransaction(_ transaction: RecurringTransaction) async {
|
func saveRecurringTransaction(_ transaction: RecurringTransaction) async {
|
||||||
self.recurringTransaction = .loading
|
self.recurringTransaction = .loading
|
||||||
do {
|
do {
|
||||||
|
@ -329,20 +389,12 @@ class DataStore : ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Published var selectedTransaction: Transaction? = nil
|
@Published var selectedTransaction: Transaction? = nil
|
||||||
private var budgetId: String = ""
|
|
||||||
private var categoryId: String? = nil
|
|
||||||
|
|
||||||
func getTransactions() async {
|
func getTransactions() async {
|
||||||
guard case let .success(budget) = self.budget else {
|
guard let budgetId = self.budgetId else {
|
||||||
self.transactions = .error(NetworkError.unknown)
|
self.transactions = .error(NetworkError.unknown)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.budgetId = budget.id
|
|
||||||
if case let .success(category) = self.category {
|
|
||||||
self.categoryId = category.id
|
|
||||||
} else {
|
|
||||||
self.categoryId = nil
|
|
||||||
}
|
|
||||||
self.transactions = .loading
|
self.transactions = .loading
|
||||||
do {
|
do {
|
||||||
var categoryIds: [String] = []
|
var categoryIds: [String] = []
|
||||||
|
|
|
@ -28,12 +28,23 @@ struct RecurringTransactionDetailsView: View {
|
||||||
Text(transaction.frequency.naturalDescription)
|
Text(transaction.frequency.naturalDescription)
|
||||||
Spacer().frame(height: 10)
|
Spacer().frame(height: 10)
|
||||||
LabeledField(label: "start", value: transaction.start.toLocaleString(), loading: .constant(false), showDivider: true)
|
LabeledField(label: "start", value: transaction.start.toLocaleString(), loading: .constant(false), showDivider: true)
|
||||||
LabeledField(label: "end", value: transaction.end?.toLocaleString(), loading: .constant(false), showDivider: true)
|
LabeledField(label: "end", value: transaction.finish?.toLocaleString(), loading: .constant(false), showDivider: true)
|
||||||
// CategoryLineItem()
|
// CategoryLineItem()
|
||||||
// BudgetLineItem()
|
// BudgetLineItem()
|
||||||
// UserLineItem()
|
// UserLineItem()
|
||||||
}.padding()
|
}.padding()
|
||||||
}
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button(action: {
|
||||||
|
Task {
|
||||||
|
await dataStore.edit(transaction)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("edit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
156
Twigs/Recurring Transactions/RecurringTransactionForm.swift
Normal file
156
Twigs/Recurring Transactions/RecurringTransactionForm.swift
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
//
|
||||||
|
// RecurringTransactionForm.swift
|
||||||
|
// Twigs
|
||||||
|
//
|
||||||
|
// Created by William Brawner on 5/20/22.
|
||||||
|
// Copyright © 2022 William Brawner. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import TwigsCore
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class RecurringTransactionForm: ObservableObject {
|
||||||
|
let apiService: TwigsApiService
|
||||||
|
let dataStore: DataStore
|
||||||
|
let transaction: TwigsCore.RecurringTransaction?
|
||||||
|
let createdBy: String
|
||||||
|
let transactionId: String
|
||||||
|
@Published var title: String
|
||||||
|
@Published var description: String
|
||||||
|
@Published var baseFrequencyUnit: String
|
||||||
|
@Published var frequencyUnit: FrequencyUnit
|
||||||
|
@Published var frequencyCount: String
|
||||||
|
@Published var daysOfWeek: Set<DayOfWeek>
|
||||||
|
@Published var dayOfMonth: DayOfMonth
|
||||||
|
@Published var dayOfYear: DayOfYear
|
||||||
|
@Published var amount: String
|
||||||
|
@Published var start: Date
|
||||||
|
@Published var endCriteria: EndCriteria
|
||||||
|
@Published var end: Date?
|
||||||
|
@Published var type: TransactionType
|
||||||
|
@Published var budgetId: String {
|
||||||
|
didSet {
|
||||||
|
updateCategories()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Published var categoryId: String
|
||||||
|
|
||||||
|
@Published var categories: AsyncData<[TwigsCore.Category]> = .empty
|
||||||
|
private var cachedCategories: [TwigsCore.Category] = []
|
||||||
|
let showDelete: Bool
|
||||||
|
|
||||||
|
init(
|
||||||
|
dataStore: DataStore,
|
||||||
|
createdBy: String,
|
||||||
|
budgetId: String,
|
||||||
|
categoryId: String? = nil,
|
||||||
|
transaction: TwigsCore.RecurringTransaction? = nil
|
||||||
|
) {
|
||||||
|
self.apiService = dataStore.apiService
|
||||||
|
self.budgetId = budgetId
|
||||||
|
self.categoryId = categoryId ?? ""
|
||||||
|
self.createdBy = createdBy
|
||||||
|
self.dataStore = dataStore
|
||||||
|
let baseTransaction = transaction ?? TwigsCore.RecurringTransaction(categoryId: categoryId, createdBy: createdBy, budgetId: budgetId)
|
||||||
|
self.transaction = transaction
|
||||||
|
self.transactionId = baseTransaction.id
|
||||||
|
self.title = baseTransaction.title
|
||||||
|
self.description = baseTransaction.description ?? ""
|
||||||
|
self.baseFrequencyUnit = baseTransaction.frequency.unit.baseName
|
||||||
|
self.frequencyUnit = baseTransaction.frequency.unit
|
||||||
|
if case let .weekly(daysOfWeek) = baseTransaction.frequency.unit {
|
||||||
|
self.daysOfWeek = daysOfWeek
|
||||||
|
} else {
|
||||||
|
self.daysOfWeek = Set()
|
||||||
|
}
|
||||||
|
if case let .monthly(dayOfMonth) = baseTransaction.frequency.unit {
|
||||||
|
self.dayOfMonth = dayOfMonth
|
||||||
|
} else {
|
||||||
|
self.dayOfMonth = DayOfMonth(day: 1)!
|
||||||
|
}
|
||||||
|
if case let .yearly(dayOfYear) = baseTransaction.frequency.unit {
|
||||||
|
self.dayOfYear = dayOfYear
|
||||||
|
} else {
|
||||||
|
self.dayOfYear = DayOfYear(month: 1, day: 1)!
|
||||||
|
}
|
||||||
|
self.frequencyCount = String(baseTransaction.frequency.count)
|
||||||
|
self.amount = baseTransaction.amountString
|
||||||
|
self.start = baseTransaction.start
|
||||||
|
self.end = baseTransaction.finish
|
||||||
|
if baseTransaction.finish != nil {
|
||||||
|
self.endCriteria = .onDate
|
||||||
|
} else {
|
||||||
|
self.endCriteria = .never
|
||||||
|
}
|
||||||
|
self.type = baseTransaction.type
|
||||||
|
self.showDelete = !baseTransaction.id.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
func load() async {
|
||||||
|
self.categories = .loading
|
||||||
|
do {
|
||||||
|
let categories = try await apiService.getCategories(budgetId: self.budgetId, expense: nil, archived: false, count: nil, page: nil)
|
||||||
|
self.cachedCategories = categories
|
||||||
|
updateCategories()
|
||||||
|
} catch {
|
||||||
|
self.categories = .error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func save() async {
|
||||||
|
let amount = Double(self.amount) ?? 0.0
|
||||||
|
var frequencyUnit: FrequencyUnit
|
||||||
|
switch self.frequencyUnit {
|
||||||
|
case .daily:
|
||||||
|
frequencyUnit = .daily
|
||||||
|
case .weekly(_):
|
||||||
|
frequencyUnit = .weekly(self.daysOfWeek)
|
||||||
|
case .monthly(_):
|
||||||
|
frequencyUnit = .monthly(self.dayOfMonth)
|
||||||
|
case .yearly(_):
|
||||||
|
frequencyUnit = .yearly(self.dayOfYear)
|
||||||
|
}
|
||||||
|
let components = Calendar.current.dateComponents([.hour, .minute, .second], from: self.start)
|
||||||
|
let time = Time(hours: components.hour!, minutes: components.minute!, seconds: components.second!)!
|
||||||
|
var end: Date? = nil
|
||||||
|
if case self.endCriteria = EndCriteria.onDate, let editedEnd = self.end, editedEnd > self.start {
|
||||||
|
end = editedEnd
|
||||||
|
}
|
||||||
|
await dataStore.saveRecurringTransaction(RecurringTransaction(
|
||||||
|
id: transactionId,
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
frequency: Frequency(unit: frequencyUnit, count: Int(frequencyCount) ?? 1, time: time)!,
|
||||||
|
start: start,
|
||||||
|
finish: end,
|
||||||
|
amount: Int(amount * 100.0),
|
||||||
|
categoryId: categoryId,
|
||||||
|
expense: type.toBool(),
|
||||||
|
createdBy: createdBy,
|
||||||
|
budgetId: budgetId
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete() async {
|
||||||
|
guard let transaction = self.transaction else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await dataStore.deleteRecurringTransaction(transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCategories() {
|
||||||
|
self.categories = .success(cachedCategories.filter {
|
||||||
|
$0.expense == self.type.toBool()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EndCriteria: String, Identifiable, CaseIterable {
|
||||||
|
var id: String {
|
||||||
|
return self.rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
case never = "never"
|
||||||
|
case onDate = "onDate"
|
||||||
|
}
|
144
Twigs/Recurring Transactions/RecurringTransactionFormView.swift
Normal file
144
Twigs/Recurring Transactions/RecurringTransactionFormView.swift
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
//
|
||||||
|
// RecurringTransactionFormView.swift
|
||||||
|
// Twigs
|
||||||
|
//
|
||||||
|
// Created by William Brawner on 5/18/22.
|
||||||
|
// Copyright © 2022 William Brawner. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import TwigsCore
|
||||||
|
|
||||||
|
struct RecurringTransactionFormView: View {
|
||||||
|
@EnvironmentObject var dataStore: DataStore
|
||||||
|
@ObservedObject var transactionForm: RecurringTransactionForm
|
||||||
|
@State private var showingAlert = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
switch self.dataStore.recurringTransaction {
|
||||||
|
case .loading:
|
||||||
|
EmbeddedLoadingView()
|
||||||
|
default:
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
TextField(LocalizedStringKey("prompt_name"), text: $transactionForm.title)
|
||||||
|
.textInputAutocapitalization(.words)
|
||||||
|
TextField(LocalizedStringKey("prompt_description"), text: $transactionForm.description)
|
||||||
|
.textInputAutocapitalization(.sentences)
|
||||||
|
TextField(LocalizedStringKey("prompt_amount"), text: $transactionForm.amount)
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
Picker(LocalizedStringKey("prompt_type"), selection: $transactionForm.type) {
|
||||||
|
ForEach(TransactionType.allCases) { type in
|
||||||
|
Text(type.localizedKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
Text("Repeat every")
|
||||||
|
TextField("count", text: $transactionForm.frequencyCount)
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
}
|
||||||
|
Picker(selection: self.$transactionForm.baseFrequencyUnit.animation(), content: {
|
||||||
|
ForEach(FrequencyUnit.allCases) {
|
||||||
|
Text(LocalizedStringKey($0.baseName)).tag($0.baseName)
|
||||||
|
}
|
||||||
|
}, label: {
|
||||||
|
Text("frequency")
|
||||||
|
})
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
FrequencyPickerView(
|
||||||
|
frequencyUnit: $transactionForm.baseFrequencyUnit,
|
||||||
|
daysOfWeek: $transactionForm.daysOfWeek,
|
||||||
|
dayOfMonth: $transactionForm.dayOfMonth,
|
||||||
|
dayOfYear: $transactionForm.dayOfYear
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Section(footer: Text("note_end_optional")) {
|
||||||
|
DatePicker(selection: $transactionForm.start, label: { Text(LocalizedStringKey("prompt_start")) })
|
||||||
|
Picker(LocalizedStringKey("prompt_end"), selection: $transactionForm.endCriteria.animation()) {
|
||||||
|
ForEach(EndCriteria.allCases) { criteria in
|
||||||
|
Text(LocalizedStringKey(criteria.rawValue)).tag(criteria)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if case .onDate = transactionForm.endCriteria {
|
||||||
|
DatePicker(
|
||||||
|
"",
|
||||||
|
selection: Binding<Date>(get: {transactionForm.end ?? Date()}, set: {transactionForm.end = $0})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
CategoryPicker(categories: $transactionForm.categories, categoryId: $transactionForm.categoryId)
|
||||||
|
}
|
||||||
|
if transactionForm.showDelete {
|
||||||
|
Button(action: {
|
||||||
|
self.showingAlert = true
|
||||||
|
}) {
|
||||||
|
Text(LocalizedStringKey("delete"))
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
.alert(isPresented:$showingAlert) {
|
||||||
|
Alert(
|
||||||
|
title: Text(LocalizedStringKey("confirm_delete")),
|
||||||
|
message: Text(LocalizedStringKey("cannot_undo")),
|
||||||
|
primaryButton: .destructive(
|
||||||
|
Text(LocalizedStringKey("delete")),
|
||||||
|
action: { Task { await transactionForm.delete() }}
|
||||||
|
),
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}.environmentObject(transactionForm)
|
||||||
|
.task {
|
||||||
|
await transactionForm.load()
|
||||||
|
}
|
||||||
|
.navigationTitle(transactionForm.transactionId.isEmpty ? "add_recurring_transaction" : "edit_recurring_transaction")
|
||||||
|
.navigationBarItems(
|
||||||
|
leading: Button("cancel", action: { dataStore.cancelEditRecurringTransaction() }),
|
||||||
|
trailing: Button("save", action: {
|
||||||
|
Task {
|
||||||
|
await transactionForm.save()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FrequencyPickerView: View {
|
||||||
|
@Binding var frequencyUnit: String
|
||||||
|
@Binding var daysOfWeek: Set<DayOfWeek>
|
||||||
|
@Binding var dayOfMonth: DayOfMonth
|
||||||
|
@Binding var dayOfYear: DayOfYear
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var body: some View {
|
||||||
|
switch frequencyUnit {
|
||||||
|
case "week":
|
||||||
|
WeeklyFrequencyPicker(selection: $daysOfWeek)
|
||||||
|
case "month":
|
||||||
|
MonthlyFrequencyPicker(dayOfMonth: $dayOfMonth)
|
||||||
|
case "year":
|
||||||
|
YearlyFrequencyPicker(dayOfYear: $dayOfYear)
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RecurringTransactionFormView_Previews: PreviewProvider {
|
||||||
|
static var dataStore = DataStore(TwigsInMemoryCacheService())
|
||||||
|
static var previews: some View {
|
||||||
|
RecurringTransactionFormView(transactionForm: RecurringTransactionForm(
|
||||||
|
dataStore: dataStore,
|
||||||
|
createdBy: MockUserRepository.currentUser.id,
|
||||||
|
budgetId: MockBudgetRepository.budget.id
|
||||||
|
)).environmentObject(dataStore)
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,29 @@ struct RecurringTransactionsListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button(action: {
|
||||||
|
dataStore.newRecurringTransaction()
|
||||||
|
}, label: {
|
||||||
|
Image(systemName: "plus").padding()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(
|
||||||
|
isPresented: $dataStore.editingRecurringTransaction,
|
||||||
|
onDismiss: {
|
||||||
|
dataStore.cancelEditRecurringTransaction()
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
RecurringTransactionFormView(transactionForm: RecurringTransactionForm(
|
||||||
|
dataStore: dataStore,
|
||||||
|
createdBy: dataStore.currentUserId ?? "",
|
||||||
|
budgetId: dataStore.budgetId ?? "",
|
||||||
|
categoryId: dataStore.selectedRecurringTransaction?.categoryId ?? dataStore.categoryId,
|
||||||
|
transaction: dataStore.selectedRecurringTransaction
|
||||||
|
))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class MockRecurringTransactionRepository: RecurringTransactionsRepository {
|
||||||
description: "A mock transaction used for testing",
|
description: "A mock transaction used for testing",
|
||||||
frequency: Frequency(unit: .daily, count: 1, time: Time(from: "09:00:00")!)!,
|
frequency: Frequency(unit: .daily, count: 1, time: Time(from: "09:00:00")!)!,
|
||||||
start: Date(),
|
start: Date(),
|
||||||
end: nil,
|
finish: nil,
|
||||||
amount: 10000,
|
amount: 10000,
|
||||||
categoryId: MockCategoryRepository.category.id,
|
categoryId: MockCategoryRepository.category.id,
|
||||||
expense: true,
|
expense: true,
|
||||||
|
|
|
@ -34,8 +34,8 @@ struct TransactionFormSheet: View {
|
||||||
Text(type.localizedKey)
|
Text(type.localizedKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BudgetPicker()
|
BudgetPicker(budgets: $transactionForm.budgets, budgetId: $transactionForm.budgetId)
|
||||||
CategoryPicker()
|
CategoryPicker(categories: $transactionForm.categories, categoryId: $transactionForm.categoryId)
|
||||||
if transactionForm.showDelete {
|
if transactionForm.showDelete {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
self.showingAlert = true
|
self.showingAlert = true
|
||||||
|
@ -76,18 +76,19 @@ struct TransactionFormSheet: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BudgetPicker: View {
|
struct BudgetPicker: View {
|
||||||
@EnvironmentObject var transactionForm: TransactionForm
|
@Binding var budgets: AsyncData<[Budget]>
|
||||||
|
@Binding var budgetId: String
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if case let .success(budgets) = self.transactionForm.budgets {
|
if case let .success(budgets) = budgets {
|
||||||
Picker(LocalizedStringKey("prompt_budget"), selection: $transactionForm.budgetId) {
|
Picker(LocalizedStringKey("prompt_budget"), selection: $budgetId) {
|
||||||
ForEach(budgets) { budget in
|
ForEach(budgets) { budget in
|
||||||
Text(budget.name)
|
Text(budget.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Picker(LocalizedStringKey("prompt_budget"), selection: $transactionForm.budgetId) {
|
Picker(LocalizedStringKey("prompt_budget"), selection: $budgetId) {
|
||||||
Text("")
|
Text("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,14 +96,15 @@ struct BudgetPicker: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CategoryPicker: View {
|
struct CategoryPicker: View {
|
||||||
@EnvironmentObject var transactionForm: TransactionForm
|
@Binding var categories: AsyncData<[TwigsCore.Category]>
|
||||||
|
@Binding var categoryId: String
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if case let .success(categories) = self.transactionForm.categories {
|
if case let .success(categories) = categories {
|
||||||
Picker(LocalizedStringKey("prompt_category"), selection: $transactionForm.categoryId) {
|
Picker(LocalizedStringKey("prompt_category"), selection: $categoryId) {
|
||||||
ForEach(categories) { category in
|
ForEach(categories) { category in
|
||||||
Text(category.title)
|
Text(category.title).tag(category.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
124
Twigs/Views/MonthlyFrequencyPicker.swift
Normal file
124
Twigs/Views/MonthlyFrequencyPicker.swift
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
//
|
||||||
|
// MonthlyFrequencyPicker.swift
|
||||||
|
// Twigs
|
||||||
|
//
|
||||||
|
// Created by William Brawner on 5/27/22.
|
||||||
|
// Copyright © 2022 William Brawner. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import TwigsCore
|
||||||
|
|
||||||
|
struct MonthlyFrequencyPicker: UIViewRepresentable {
|
||||||
|
@Binding var dayOfMonth: DayOfMonth
|
||||||
|
@State var dayOfWeek: Int
|
||||||
|
@State var intDay: Int
|
||||||
|
@State var ordinalDay: Int
|
||||||
|
|
||||||
|
init(dayOfMonth: Binding<DayOfMonth>) {
|
||||||
|
self._dayOfMonth = dayOfMonth
|
||||||
|
if case let .fixed(intDay) = dayOfMonth.wrappedValue {
|
||||||
|
self.intDay = intDay - 1
|
||||||
|
self.ordinalDay = 0
|
||||||
|
self.dayOfWeek = 0
|
||||||
|
} else if case let .ordinal(ordinalDay, dayOfWeek) = dayOfMonth.wrappedValue {
|
||||||
|
self.intDay = 0
|
||||||
|
self.ordinalDay = Ordinal.allCases.firstIndex(of: ordinalDay)!
|
||||||
|
self.dayOfWeek = DayOfWeek.allCases.firstIndex(of: dayOfWeek)!
|
||||||
|
} else {
|
||||||
|
self.intDay = 0
|
||||||
|
self.dayOfWeek = 0
|
||||||
|
self.ordinalDay = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> MonthlyFrequencyPicker.Coordinator {
|
||||||
|
Coordinator(self, selectedOrdinal: $ordinalDay, selectedDay: $intDay, selectedDayOfWeek: $dayOfWeek)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIView(context: UIViewRepresentableContext<MonthlyFrequencyPicker>) -> UIPickerView {
|
||||||
|
let picker = UIPickerView(frame: .zero)
|
||||||
|
picker.dataSource = context.coordinator
|
||||||
|
picker.delegate = context.coordinator
|
||||||
|
return picker
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ view: UIPickerView, context: UIViewRepresentableContext<MonthlyFrequencyPicker>) {
|
||||||
|
view.selectRow(ordinalDay, inComponent: 0, animated: false)
|
||||||
|
let component2Selection = ordinalDay == 0 ? intDay : dayOfWeek
|
||||||
|
view.selectRow(component2Selection, inComponent: 1, animated: true)
|
||||||
|
view.reloadComponent(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
|
||||||
|
let ordinals = Ordinal.allCases.map {
|
||||||
|
$0.rawValue.lowercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent: MonthlyFrequencyPicker
|
||||||
|
@Binding var selectedOrdinal: Int {
|
||||||
|
didSet {
|
||||||
|
// This is a workaround for the pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) function not getting the correct values for the selectedOrdinal
|
||||||
|
ordinal = self.selectedOrdinal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Binding var selectedDay: Int
|
||||||
|
@Binding var selectedDayOfWeek: Int
|
||||||
|
|
||||||
|
private var ordinal = 0
|
||||||
|
|
||||||
|
init(_ pickerView: MonthlyFrequencyPicker, selectedOrdinal: Binding<Int>, selectedDay: Binding<Int>, selectedDayOfWeek: Binding<Int>) {
|
||||||
|
self.parent = pickerView
|
||||||
|
self._selectedOrdinal = selectedOrdinal
|
||||||
|
self._selectedDay = selectedDay
|
||||||
|
self._selectedDayOfWeek = selectedDayOfWeek
|
||||||
|
}
|
||||||
|
|
||||||
|
func numberOfComponents(in pickerView: UIPickerView) -> Int {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
||||||
|
if component == 0 {
|
||||||
|
return ordinals.count
|
||||||
|
}
|
||||||
|
if ordinal == 0 {
|
||||||
|
return 31
|
||||||
|
} else {
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
||||||
|
if component == 0 {
|
||||||
|
return NSLocalizedString(ordinals[row], comment: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ordinal == 0 {
|
||||||
|
return String(row + 1)
|
||||||
|
} else {
|
||||||
|
return NSLocalizedString(DayOfWeek.allCases[row].rawValue, comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
||||||
|
if component == 0 {
|
||||||
|
selectedOrdinal = row
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ordinal == 0 {
|
||||||
|
selectedDay = row
|
||||||
|
} else {
|
||||||
|
selectedDayOfWeek = row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MonthlyFrequencyPicker_Previews: PreviewProvider {
|
||||||
|
@State static var dayOfMonth: DayOfMonth = .fixed(1)
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
MonthlyFrequencyPicker(dayOfMonth: $dayOfMonth)
|
||||||
|
}
|
||||||
|
}
|
65
Twigs/Views/MultiPicker.swift
Normal file
65
Twigs/Views/MultiPicker.swift
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
//
|
||||||
|
// MultiPicker.swift
|
||||||
|
// Twigs
|
||||||
|
//
|
||||||
|
// Created by William Brawner on 5/20/22.
|
||||||
|
// Copyright © 2022 William Brawner. All rights reserved.
|
||||||
|
// Adapted from https://stackoverflow.com/a/58664469
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MultiPicker: UIViewRepresentable {
|
||||||
|
var data: [[String]]
|
||||||
|
@Binding var selections: [Int]
|
||||||
|
|
||||||
|
func makeCoordinator() -> MultiPicker.Coordinator {
|
||||||
|
Coordinator(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIView(context: UIViewRepresentableContext<MultiPicker>) -> UIPickerView {
|
||||||
|
let picker = UIPickerView(frame: .zero)
|
||||||
|
|
||||||
|
picker.dataSource = context.coordinator
|
||||||
|
picker.delegate = context.coordinator
|
||||||
|
|
||||||
|
return picker
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ view: UIPickerView, context: UIViewRepresentableContext<MultiPicker>) {
|
||||||
|
for i in 0...(self.selections.count - 1) {
|
||||||
|
view.selectRow(self.selections[i], inComponent: i, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
|
||||||
|
var parent: MultiPicker
|
||||||
|
|
||||||
|
init(_ pickerView: MultiPicker) {
|
||||||
|
self.parent = pickerView
|
||||||
|
}
|
||||||
|
|
||||||
|
func numberOfComponents(in pickerView: UIPickerView) -> Int {
|
||||||
|
return self.parent.data.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
||||||
|
return self.parent.data[component].count
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
||||||
|
return self.parent.data[component][row]
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
||||||
|
self.parent.selections[component] = row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MultiPicker_Previews: PreviewProvider {
|
||||||
|
@State static var selections: [Int] = [0, 0]
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
MultiPicker(data: [["a", "b", "c"], ["one", "two", "three"]], selections: $selections)
|
||||||
|
}
|
||||||
|
}
|
62
Twigs/Views/WeeklyFrequencyPicker.swift
Normal file
62
Twigs/Views/WeeklyFrequencyPicker.swift
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
//
|
||||||
|
// WeeklyFrequencyPicker.swift
|
||||||
|
// Twigs
|
||||||
|
//
|
||||||
|
// Created by William Brawner on 5/27/22.
|
||||||
|
// Copyright © 2022 William Brawner. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import TwigsCore
|
||||||
|
|
||||||
|
struct WeeklyFrequencyPicker: View {
|
||||||
|
@Binding var selection: Set<DayOfWeek>
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
ForEach(DayOfWeek.allCases.slice(count: 4, page: 1)) { dayOfWeek in
|
||||||
|
Toggle(isOn: .constant(selection.contains(dayOfWeek))) {
|
||||||
|
Text(LocalizedStringKey(dayOfWeek.rawValue.lowercased()))
|
||||||
|
.lineLimit(1)
|
||||||
|
.onTapGesture {
|
||||||
|
if selection.contains(dayOfWeek) {
|
||||||
|
selection.remove(dayOfWeek)
|
||||||
|
} else {
|
||||||
|
selection.update(with: dayOfWeek)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toggleStyle(.button)
|
||||||
|
.onSubmit {
|
||||||
|
print("Toggle selected for \(dayOfWeek)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
ForEach(DayOfWeek.allCases.slice(count: 4, page: 2)) { dayOfWeek in
|
||||||
|
Toggle(isOn: .constant(selection.contains(dayOfWeek))) {
|
||||||
|
Text(LocalizedStringKey(dayOfWeek.rawValue.lowercased()))
|
||||||
|
.lineLimit(1)
|
||||||
|
.onTapGesture {
|
||||||
|
if selection.contains(dayOfWeek) {
|
||||||
|
selection.remove(dayOfWeek)
|
||||||
|
} else {
|
||||||
|
selection.update(with: dayOfWeek)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toggleStyle(.button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WeeklyFrequencyPicker_Previews: PreviewProvider {
|
||||||
|
@State static var selection: Set<DayOfWeek> = Set()
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
WeeklyFrequencyPicker(selection: $selection)
|
||||||
|
}
|
||||||
|
}
|
118
Twigs/Views/YearlyFrequencyPicker.swift
Normal file
118
Twigs/Views/YearlyFrequencyPicker.swift
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
//
|
||||||
|
// YearlyFrequencyPicker.swift
|
||||||
|
// Twigs
|
||||||
|
//
|
||||||
|
// Created by William Brawner on 5/27/22.
|
||||||
|
// Copyright © 2022 William Brawner. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import TwigsCore
|
||||||
|
|
||||||
|
struct YearlyFrequencyPicker: UIViewRepresentable {
|
||||||
|
@Binding var dayOfYear: DayOfYear
|
||||||
|
@State var selectedMonth: Int {
|
||||||
|
didSet {
|
||||||
|
selectedDay = min(self.selectedDay, DayOfYear.maxDays(inMonth: self.selectedMonth + 1) - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@State var selectedDay: Int {
|
||||||
|
didSet {
|
||||||
|
if let dayOfYear = DayOfYear(month: selectedMonth + 1, day: selectedDay + 1) {
|
||||||
|
self.dayOfYear = dayOfYear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(dayOfYear: Binding<DayOfYear>) {
|
||||||
|
self._dayOfYear = dayOfYear
|
||||||
|
self.selectedMonth = dayOfYear.wrappedValue.month - 1
|
||||||
|
self.selectedDay = dayOfYear.wrappedValue.day - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> YearlyFrequencyPicker.Coordinator {
|
||||||
|
Coordinator(self, selectedMonth: $selectedMonth, selectedDay: $selectedDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIView(context: UIViewRepresentableContext<YearlyFrequencyPicker>) -> UIPickerView {
|
||||||
|
let picker = UIPickerView(frame: .zero)
|
||||||
|
picker.dataSource = context.coordinator
|
||||||
|
picker.delegate = context.coordinator
|
||||||
|
return picker
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ view: UIPickerView, context: UIViewRepresentableContext<YearlyFrequencyPicker>) {
|
||||||
|
view.selectRow(selectedMonth, inComponent: 0, animated: false)
|
||||||
|
view.selectRow(selectedDay, inComponent: 1, animated: true)
|
||||||
|
view.reloadComponent(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
|
||||||
|
let months = [
|
||||||
|
"january",
|
||||||
|
"february",
|
||||||
|
"march",
|
||||||
|
"april",
|
||||||
|
"may",
|
||||||
|
"june",
|
||||||
|
"july",
|
||||||
|
"august",
|
||||||
|
"september",
|
||||||
|
"october",
|
||||||
|
"november",
|
||||||
|
"december"
|
||||||
|
]
|
||||||
|
|
||||||
|
var parent: YearlyFrequencyPicker
|
||||||
|
@Binding var selectedMonth: Int {
|
||||||
|
didSet {
|
||||||
|
// This is a workaround for the pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) function not getting the correct values for selectedMonth
|
||||||
|
month = self.selectedMonth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Binding var selectedDay: Int
|
||||||
|
|
||||||
|
private var month = 0
|
||||||
|
|
||||||
|
init(_ pickerView: YearlyFrequencyPicker, selectedMonth: Binding<Int>, selectedDay: Binding<Int> ) {
|
||||||
|
self.parent = pickerView
|
||||||
|
self._selectedMonth = selectedMonth
|
||||||
|
self._selectedDay = selectedDay
|
||||||
|
}
|
||||||
|
|
||||||
|
func numberOfComponents(in pickerView: UIPickerView) -> Int {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
||||||
|
if component == 0 {
|
||||||
|
return months.count
|
||||||
|
}
|
||||||
|
return DayOfYear.maxDays(inMonth: month + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
||||||
|
if component == 0 {
|
||||||
|
return NSLocalizedString(months[row], comment: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(row + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
||||||
|
if component == 0 {
|
||||||
|
selectedMonth = row
|
||||||
|
} else {
|
||||||
|
selectedDay = row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct YearlyFrequencyPicker_Previews: PreviewProvider {
|
||||||
|
@State static var dayOfYear: DayOfYear = (DayOfYear(month: 1, day: 1)!)
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
YearlyFrequencyPicker(dayOfYear: $dayOfYear)
|
||||||
|
}
|
||||||
|
}
|
|
@ -80,3 +80,54 @@
|
||||||
// MARK: Recurring Transactions
|
// MARK: Recurring Transactions
|
||||||
"recurring" = "Recurring";
|
"recurring" = "Recurring";
|
||||||
"recurring_transactions" = "Recurring Transactions";
|
"recurring_transactions" = "Recurring Transactions";
|
||||||
|
"add_recurring_transaction" = "Add Recurring Transaction";
|
||||||
|
"edit_recurring_transaction" = "Edit Recurring Transaction";
|
||||||
|
"prompt_start" = "Start";
|
||||||
|
"prompt_end" = "End";
|
||||||
|
"frequency" = "Frequency";
|
||||||
|
"note_end_optional" = "Note: The end date is optional";
|
||||||
|
|
||||||
|
"day" = "Day";
|
||||||
|
"week" = "Week";
|
||||||
|
"month" = "Month";
|
||||||
|
"year" = "Year";
|
||||||
|
|
||||||
|
"sunday" = "SUN";
|
||||||
|
"monday" = "MON";
|
||||||
|
"tuesday" = "TUES";
|
||||||
|
"wednesday" = "WED";
|
||||||
|
"thursday" = "THURS";
|
||||||
|
"friday" = "FRI";
|
||||||
|
"saturday" = "SAT";
|
||||||
|
|
||||||
|
"first" = "First";
|
||||||
|
"second" = "Second";
|
||||||
|
"third" = "Third";
|
||||||
|
"fourth" = "Fourth";
|
||||||
|
"last" = "Last";
|
||||||
|
|
||||||
|
"SUNDAY" = "Sunday";
|
||||||
|
"MONDAY" = "Monday";
|
||||||
|
"TUESDAY" = "Tuesday";
|
||||||
|
"WEDNESDAY" = "Wednesday";
|
||||||
|
"THURSDAY" = "Thursday";
|
||||||
|
"FRIDAY" = "Friday";
|
||||||
|
"SATURDAY" = "Saturday";
|
||||||
|
|
||||||
|
"january" = "January";
|
||||||
|
"february" = "February";
|
||||||
|
"march" = "March";
|
||||||
|
"april" = "April";
|
||||||
|
"may" = "May";
|
||||||
|
"june" = "June";
|
||||||
|
"july" = "July";
|
||||||
|
"august" = "August";
|
||||||
|
"september" = "September";
|
||||||
|
"october" = "October";
|
||||||
|
"november" = "November";
|
||||||
|
"december" = "December";
|
||||||
|
|
||||||
|
"start" = "Start";
|
||||||
|
"end" = "End";
|
||||||
|
"never" = "Never";
|
||||||
|
"onDate" = "On Date";
|
||||||
|
|
|
@ -81,3 +81,54 @@
|
||||||
// MARK: Recurring Transactions
|
// MARK: Recurring Transactions
|
||||||
"recurring" = "Recurrentes";
|
"recurring" = "Recurrentes";
|
||||||
"recurring_transactions" = "Transacciones Recurrentes";
|
"recurring_transactions" = "Transacciones Recurrentes";
|
||||||
|
"add_recurring_transaction" = "Agregar Transacción Recurrente";
|
||||||
|
"edit_recurring_transaction" = "Editar Transacción Recurrente";
|
||||||
|
"prompt_start" = "Inicio";
|
||||||
|
"prompt_end" = "Final";
|
||||||
|
"note_end_optional" = "Nota: La fecha final es opcional";
|
||||||
|
"frequency" = "Frequencia";
|
||||||
|
|
||||||
|
"day" = "Día";
|
||||||
|
"week" = "Semana";
|
||||||
|
"month" = "Mes";
|
||||||
|
"year" = "Año";
|
||||||
|
|
||||||
|
"sunday" = "DOM";
|
||||||
|
"monday" = "LUN";
|
||||||
|
"tuesday" = "MAR";
|
||||||
|
"wednesday" = "MIE";
|
||||||
|
"thursday" = "JUE";
|
||||||
|
"friday" = "VIE";
|
||||||
|
"saturday" = "SAB";
|
||||||
|
|
||||||
|
"first" = "Primer";
|
||||||
|
"second" = "Segundo";
|
||||||
|
"third" = "Tercer";
|
||||||
|
"fourth" = "Cuarto";
|
||||||
|
"last" = "Último";
|
||||||
|
|
||||||
|
"SUNDAY" = "domingo";
|
||||||
|
"MONDAY" = "lunes";
|
||||||
|
"TUESDAY" = "martes";
|
||||||
|
"WEDNESDAY" = "miércoles";
|
||||||
|
"THURSDAY" = "jueves";
|
||||||
|
"FRIDAY" = "viernes";
|
||||||
|
"SATURDAY" = "sábado";
|
||||||
|
|
||||||
|
"january" = "enero";
|
||||||
|
"february" = "febrero";
|
||||||
|
"march" = "marzo";
|
||||||
|
"april" = "abril";
|
||||||
|
"may" = "mayo";
|
||||||
|
"june" = "junio";
|
||||||
|
"july" = "julio";
|
||||||
|
"august" = "agosto";
|
||||||
|
"september" = "septiembre";
|
||||||
|
"october" = "octubre";
|
||||||
|
"november" = "noviembre";
|
||||||
|
"december" = "diciembre";
|
||||||
|
|
||||||
|
"start" = "Inicio";
|
||||||
|
"end" = "Fin";
|
||||||
|
"never" = "Nunca";
|
||||||
|
"onDate" = "Fecha";
|
||||||
|
|
Loading…
Reference in a new issue