Combine all top-level datastores together
This commit is contained in:
parent
2cffa9c83c
commit
f62670aaec
32 changed files with 629 additions and 711 deletions
|
@ -16,7 +16,6 @@
|
|||
284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2841022F2342D97300EAFA29 /* BudgetListsView.swift */; };
|
||||
284102322342E12F00EAFA29 /* CategoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284102312342E12F00EAFA29 /* CategoryListView.swift */; };
|
||||
2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2857EAEC233DA30B0026BC83 /* LoadingView.swift */; };
|
||||
289510242352AAFC00BC862B /* UserDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 289510232352AAFC00BC862B /* UserDataStore.swift */; };
|
||||
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC94F1233C373900BFB70A /* LoginView.swift */; };
|
||||
28AC94F4233C373A00BFB70A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 28AC94F3233C373A00BFB70A /* Assets.xcassets */; };
|
||||
28AC94F7233C373A00BFB70A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 28AC94F6233C373A00BFB70A /* Preview Assets.xcassets */; };
|
||||
|
@ -28,20 +27,15 @@
|
|||
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC952B233C434800BFB70A /* UserRepository.swift */; };
|
||||
28B9E50E2346BCB2007C3909 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B9E50D2346BCB2007C3909 /* RegistrationView.swift */; };
|
||||
28CE8B9523525F990072BC4C /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28CE8B9423525F990072BC4C /* Extensions.swift */; };
|
||||
28FE6AF42342E3CB00D5543E /* BudgetsDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF32342E3CB00D5543E /* BudgetsDataStore.swift */; };
|
||||
28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */; };
|
||||
28FE6AFA23441E3700D5543E /* CategoryListDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF923441E3700D5543E /* CategoryListDataStore.swift */; };
|
||||
28FE6AFC23441E4500D5543E /* CategoryRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AFB23441E4500D5543E /* CategoryRepository.swift */; };
|
||||
28FE6B002344308600D5543E /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AFF2344308600D5543E /* Transaction.swift */; };
|
||||
28FE6B022344331B00D5543E /* TransactionDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B012344331B00D5543E /* TransactionDataStore.swift */; };
|
||||
28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B03234449DC00D5543E /* TransactionListView.swift */; };
|
||||
28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */; };
|
||||
543ECE42233E82A40018A9D9 /* AuthenticationDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */; };
|
||||
8005FD5D277EAB0200E48B23 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8005FD5C277EAB0200E48B23 /* MainView.swift */; };
|
||||
800DFC2C277FF47A00EDCE9B /* AsyncData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800DFC2B277FF47A00EDCE9B /* AsyncData.swift */; };
|
||||
801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */; };
|
||||
801D08CE275F189E00931465 /* RecurringTransactionsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */; };
|
||||
801D08D0275F1AE300931465 /* RecurringTransactionDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */; };
|
||||
801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */; };
|
||||
802161D0277647920075761A /* AsyncObservableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 802161CF277647920075761A /* AsyncObservableObject.swift */; };
|
||||
8021EFAD280A0FCA00043F18 /* TwigsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 8021EFAC280A0FCA00043F18 /* TwigsCore */; };
|
||||
|
@ -54,10 +48,12 @@
|
|||
8076A84F2809FE8E006B9DC9 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A84E2809FE8E006B9DC9 /* ArgumentParser */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */; };
|
||||
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D2CE192833448500EDD6C2 /* DataStore.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -100,7 +96,6 @@
|
|||
2841022F2342D97300EAFA29 /* BudgetListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetListsView.swift; sourceTree = "<group>"; };
|
||||
284102312342E12F00EAFA29 /* CategoryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryListView.swift; sourceTree = "<group>"; };
|
||||
2857EAEC233DA30B0026BC83 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
|
||||
289510232352AAFC00BC862B /* UserDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataStore.swift; sourceTree = "<group>"; };
|
||||
28AC94EA233C373900BFB70A /* Twigs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Twigs.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
28AC94F1233C373900BFB70A /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
|
||||
28AC94F3233C373A00BFB70A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
|
@ -118,20 +113,15 @@
|
|||
28AC952B233C434800BFB70A /* UserRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = "<group>"; };
|
||||
28B9E50D2346BCB2007C3909 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
||||
28CE8B9423525F990072BC4C /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
28FE6AF32342E3CB00D5543E /* BudgetsDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetsDataStore.swift; sourceTree = "<group>"; };
|
||||
28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetRepository.swift; sourceTree = "<group>"; };
|
||||
28FE6AF923441E3700D5543E /* CategoryListDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryListDataStore.swift; sourceTree = "<group>"; };
|
||||
28FE6AFB23441E4500D5543E /* CategoryRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryRepository.swift; sourceTree = "<group>"; };
|
||||
28FE6AFF2344308600D5543E /* Transaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = "<group>"; };
|
||||
28FE6B012344331B00D5543E /* TransactionDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDataStore.swift; sourceTree = "<group>"; };
|
||||
28FE6B03234449DC00D5543E /* TransactionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionListView.swift; sourceTree = "<group>"; };
|
||||
28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailsView.swift; sourceTree = "<group>"; };
|
||||
543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationDataStore.swift; sourceTree = "<group>"; };
|
||||
8005FD5C277EAB0200E48B23 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
|
||||
800DFC2B277FF47A00EDCE9B /* AsyncData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncData.swift; sourceTree = "<group>"; };
|
||||
801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsListView.swift; sourceTree = "<group>"; };
|
||||
801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsRepository.swift; sourceTree = "<group>"; };
|
||||
801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionDataStore.swift; sourceTree = "<group>"; };
|
||||
801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionDetailsView.swift; sourceTree = "<group>"; };
|
||||
802161CF277647920075761A /* AsyncObservableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncObservableObject.swift; sourceTree = "<group>"; };
|
||||
8021EFAB280A0FA100043F18 /* TwigsCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TwigsCore; path = ../TwigsCore; sourceTree = "<group>"; };
|
||||
|
@ -142,11 +132,13 @@
|
|||
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>"; };
|
||||
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; };
|
||||
809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryFormSheet.swift; sourceTree = "<group>"; };
|
||||
809B94242722597800B1DAE2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
80A419EA2787C0A00090C515 /* twigs-cli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "twigs-cli"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
80A419EC2787C0A00090C515 /* TwigsCli.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsCli.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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -164,6 +156,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
808CA1A728354005002EDD59 /* XCTest.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -197,7 +190,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
2841022F2342D97300EAFA29 /* BudgetListsView.swift */,
|
||||
28FE6AF32342E3CB00D5543E /* BudgetsDataStore.swift */,
|
||||
28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */,
|
||||
282126BA235CDD3C00072D52 /* BudgetDetailsView.swift */,
|
||||
);
|
||||
|
@ -207,7 +199,6 @@
|
|||
2841022A2342D8CB00EAFA29 /* Category */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
28FE6AF923441E3700D5543E /* CategoryListDataStore.swift */,
|
||||
8043EB83271F26ED00498E73 /* CategoryDetailsView.swift */,
|
||||
284102312342E12F00EAFA29 /* CategoryListView.swift */,
|
||||
28FE6AFB23441E4500D5543E /* CategoryRepository.swift */,
|
||||
|
@ -278,6 +269,7 @@
|
|||
2857EAEB233DA2F90026BC83 /* Views */,
|
||||
802161CF277647920075761A /* AsyncObservableObject.swift */,
|
||||
800DFC2B277FF47A00EDCE9B /* AsyncData.swift */,
|
||||
80D2CE192833448500EDD6C2 /* DataStore.swift */,
|
||||
);
|
||||
path = Twigs;
|
||||
sourceTree = "<group>";
|
||||
|
@ -313,7 +305,6 @@
|
|||
children = (
|
||||
28AC9528233C433400BFB70A /* TransactionRepository.swift */,
|
||||
28FE6AFF2344308600D5543E /* Transaction.swift */,
|
||||
28FE6B012344331B00D5543E /* TransactionDataStore.swift */,
|
||||
28FE6B03234449DC00D5543E /* TransactionListView.swift */,
|
||||
28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */,
|
||||
2821265F23555FD300072D52 /* TransactionFormSheet.swift */,
|
||||
|
@ -335,8 +326,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
28AC952B233C434800BFB70A /* UserRepository.swift */,
|
||||
543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */,
|
||||
289510232352AAFC00BC862B /* UserDataStore.swift */,
|
||||
);
|
||||
path = User;
|
||||
sourceTree = "<group>";
|
||||
|
@ -344,6 +333,7 @@
|
|||
8005FD59277E623900E48B23 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
808CA1A628354005002EDD59 /* XCTest.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
@ -361,7 +351,6 @@
|
|||
children = (
|
||||
801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */,
|
||||
801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */,
|
||||
801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */,
|
||||
801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */,
|
||||
);
|
||||
path = "Recurring Transactions";
|
||||
|
@ -552,31 +541,26 @@
|
|||
801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */,
|
||||
28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */,
|
||||
80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */,
|
||||
28FE6AF42342E3CB00D5543E /* BudgetsDataStore.swift in Sources */,
|
||||
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */,
|
||||
28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */,
|
||||
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */,
|
||||
802161D0277647920075761A /* AsyncObservableObject.swift in Sources */,
|
||||
282126BB235CDD3C00072D52 /* BudgetDetailsView.swift in Sources */,
|
||||
801D08D0275F1AE300931465 /* RecurringTransactionDataStore.swift in Sources */,
|
||||
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */,
|
||||
2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */,
|
||||
282126A3235ABC1800072D52 /* TwigsInMemoryCacheService.swift in Sources */,
|
||||
800DFC2C277FF47A00EDCE9B /* AsyncData.swift in Sources */,
|
||||
8044BA3B2784B659009A78D4 /* TransactionDetails.swift in Sources */,
|
||||
28FE6AFA23441E3700D5543E /* CategoryListDataStore.swift in Sources */,
|
||||
28B9E50E2346BCB2007C3909 /* RegistrationView.swift in Sources */,
|
||||
284102322342E12F00EAFA29 /* CategoryListView.swift in Sources */,
|
||||
8043EB84271F26ED00498E73 /* CategoryDetailsView.swift in Sources */,
|
||||
289510242352AAFC00BC862B /* UserDataStore.swift in Sources */,
|
||||
28FE6B002344308600D5543E /* Transaction.swift in Sources */,
|
||||
8005FD5D277EAB0200E48B23 /* MainView.swift in Sources */,
|
||||
282126A1235929B800072D52 /* ProfileView.swift in Sources */,
|
||||
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */,
|
||||
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */,
|
||||
28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */,
|
||||
28FE6B022344331B00D5543E /* TransactionDataStore.swift in Sources */,
|
||||
8044BA3F27853054009A78D4 /* CategoryForm.swift in Sources */,
|
||||
543ECE42233E82A40018A9D9 /* AuthenticationDataStore.swift in Sources */,
|
||||
80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */,
|
||||
8044BA3927828E9D009A78D4 /* CategoryDataStore.swift in Sources */,
|
||||
284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */,
|
||||
|
|
|
@ -10,14 +10,14 @@ import SwiftUI
|
|||
import TwigsCore
|
||||
|
||||
struct BudgetDetailsView: View {
|
||||
@EnvironmentObject var budgetDataStore: BudgetsDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
let budget: Budget
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
InlineLoadingView(
|
||||
data: self.$budgetDataStore.overview,
|
||||
action: { await self.budgetDataStore.loadOverview(self.budget) },
|
||||
data: self.$dataStore.overview,
|
||||
action: { await self.dataStore.loadOverview(self.budget) },
|
||||
errorTextLocalizedStringKey: "budgets_load_failure"
|
||||
) { overview in
|
||||
List {
|
||||
|
@ -96,7 +96,7 @@ struct ExpensesOverview: View {
|
|||
struct BudgetDetailsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BudgetDetailsView(budget: MockBudgetRepository.budget)
|
||||
.environmentObject(BudgetsDataStore(budgetRepository: MockBudgetRepository(), categoryRepository: MockCategoryRepository(), transactionRepository: MockTransactionRepository()))
|
||||
.environmentObject(TwigsInMemoryCacheService())
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -12,12 +12,12 @@ import Combine
|
|||
import TwigsCore
|
||||
|
||||
struct BudgetListsView: View {
|
||||
@EnvironmentObject var budgetDataStore: BudgetsDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
|
||||
var body: some View {
|
||||
InlineLoadingView(
|
||||
data: $budgetDataStore.budgets,
|
||||
action: { await self.budgetDataStore.getBudgets(count: nil, page: nil) },
|
||||
data: $dataStore.budgets,
|
||||
action: { await self.dataStore.getBudgets(count: nil, page: nil) },
|
||||
errorTextLocalizedStringKey: "budgets_load_failure"
|
||||
) { (budgets: [Budget]) in
|
||||
Section("budgets") {
|
||||
|
@ -30,13 +30,13 @@ struct BudgetListsView: View {
|
|||
}
|
||||
|
||||
struct BudgetListItemView: View {
|
||||
@EnvironmentObject var budgetDataStore: BudgetsDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
let budget: Budget
|
||||
|
||||
var body: some View {
|
||||
Button(
|
||||
action: {
|
||||
self.budgetDataStore.selectBudget(budget)
|
||||
self.dataStore.selectBudget(budget)
|
||||
},
|
||||
label: {
|
||||
VStack(alignment: .leading) {
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
//
|
||||
// BudgetsDataStore.swift
|
||||
// Budget
|
||||
//
|
||||
// Created by Billy Brawner on 9/30/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import TwigsCore
|
||||
|
||||
private let LAST_BUDGET = "LAST_BUDGET"
|
||||
|
||||
@MainActor
|
||||
class BudgetsDataStore: ObservableObject {
|
||||
private let budgetRepository: BudgetRepository
|
||||
private let categoryRepository: CategoryRepository
|
||||
private let transactionRepository: TransactionRepository
|
||||
@Published var budgets: AsyncData<[Budget]> = .empty
|
||||
@Published var budget: AsyncData<Budget> = .empty {
|
||||
didSet {
|
||||
self.overview = .empty
|
||||
if case let .success(budget) = self.budget {
|
||||
UserDefaults.standard.set(budget.id, forKey: LAST_BUDGET)
|
||||
self.showBudgetSelection = false
|
||||
Task {
|
||||
await loadOverview(budget)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var overview: AsyncData<BudgetOverview> = .empty
|
||||
@Published var showBudgetSelection: Bool = true
|
||||
|
||||
init(budgetRepository: BudgetRepository, categoryRepository: CategoryRepository, transactionRepository: TransactionRepository) {
|
||||
self.budgetRepository = budgetRepository
|
||||
self.categoryRepository = categoryRepository
|
||||
self.transactionRepository = transactionRepository
|
||||
}
|
||||
|
||||
func getBudgets(count: Int? = nil, page: Int? = nil) async {
|
||||
// TODO: Find some way to extract this to a generic function
|
||||
self.budgets = .loading
|
||||
do {
|
||||
let budgets = try await self.budgetRepository.getBudgets(count: count, page: page).sorted(by: { $0.name < $1.name })
|
||||
self.budgets = .success(budgets)
|
||||
if self.budget != .empty {
|
||||
return
|
||||
}
|
||||
if let id = UserDefaults.standard.string(forKey: LAST_BUDGET), let lastBudget = budgets.first(where: { $0.id == id }) {
|
||||
self.budget = .success(lastBudget)
|
||||
} else {
|
||||
if let budget = budgets.first {
|
||||
self.budget = .success(budget)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
self.budgets = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func loadOverview(_ budget: Budget) async {
|
||||
self.overview = .loading
|
||||
do {
|
||||
let budgetBalance = try await self.transactionRepository.sumTransactions(budgetId: budget.id, categoryId: nil, from: nil, to: nil)
|
||||
let categories = try await self.categoryRepository.getCategories(budgetId: budget.id, expense: nil, archived: false, count: nil, page: nil)
|
||||
var budgetOverview = BudgetOverview(budget: budget, balance: budgetBalance.balance)
|
||||
try await withThrowingTaskGroup(of: (TwigsCore.Category, BalanceResponse).self) { group in
|
||||
for category in categories {
|
||||
group.addTask {
|
||||
return (category, try await self.transactionRepository.sumTransactions(budgetId: nil, categoryId: category.id, from: nil, to: nil))
|
||||
}
|
||||
}
|
||||
|
||||
for try await (category, response) in group {
|
||||
if category.expense {
|
||||
budgetOverview.expectedExpenses += category.amount
|
||||
} else {
|
||||
budgetOverview.expectedIncome += category.amount
|
||||
}
|
||||
|
||||
if category.expense {
|
||||
budgetOverview.actualExpenses += abs(response.balance)
|
||||
} else {
|
||||
budgetOverview.actualIncome += response.balance
|
||||
}
|
||||
}
|
||||
}
|
||||
self.overview = .success(budgetOverview)
|
||||
} catch {
|
||||
self.overview = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func selectBudget(_ budget: Budget) {
|
||||
self.budget = .success(budget)
|
||||
}
|
||||
}
|
|
@ -12,16 +12,16 @@ import TwigsCore
|
|||
@MainActor
|
||||
class CategoryDataStore: ObservableObject {
|
||||
@Published var sum: AsyncData<Int> = .empty
|
||||
let transactionRepository: TransactionRepository
|
||||
let apiService: TwigsApiService
|
||||
|
||||
init(transactionRepository: TransactionRepository) {
|
||||
self.transactionRepository = transactionRepository
|
||||
init(_ apiService: TwigsApiService) {
|
||||
self.apiService = apiService
|
||||
}
|
||||
|
||||
func sum(categoryId: String, from: Date? = nil, to: Date? = nil) async {
|
||||
self.sum = .loading
|
||||
do {
|
||||
let sum = try await self.transactionRepository.sumTransactions(budgetId: nil, categoryId: categoryId, from: from, to: to).balance
|
||||
let sum = try await self.apiService.sumTransactions(budgetId: nil, categoryId: categoryId, from: from, to: to).balance
|
||||
self.sum = .success(sum)
|
||||
} catch {
|
||||
self.sum = .error(error)
|
||||
|
|
|
@ -10,9 +10,8 @@ import SwiftUI
|
|||
import TwigsCore
|
||||
|
||||
struct CategoryDetailsView: View {
|
||||
@EnvironmentObject var categoryListDataStore: CategoryListDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
@EnvironmentObject var categoryDataStore: CategoryDataStore
|
||||
@EnvironmentObject var transactionDataStore: TransactionDataStore
|
||||
@EnvironmentObject var apiService: TwigsApiService
|
||||
let budget: Budget
|
||||
@State var sum: Int? = 0
|
||||
|
@ -35,8 +34,8 @@ struct CategoryDetailsView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
if let category = categoryListDataStore.selectedCategory {
|
||||
TransactionListView(apiService: apiService, budget: budget, category: category) {
|
||||
if let category = dataStore.selectedCategory {
|
||||
TransactionListView() {
|
||||
VStack {
|
||||
Text(verbatim: category.description ?? "")
|
||||
.padding()
|
||||
|
@ -59,7 +58,7 @@ struct CategoryDetailsView: View {
|
|||
}, content: {
|
||||
CategoryFormSheet(categoryForm: CategoryForm(
|
||||
category: category,
|
||||
categoryList: categoryListDataStore,
|
||||
dataStore: dataStore,
|
||||
budgetId: category.budgetId
|
||||
))
|
||||
})
|
||||
|
@ -86,7 +85,7 @@ struct LabeledCounter: View {
|
|||
struct CategoryDetailsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CategoryDetailsView(MockBudgetRepository.budget)
|
||||
.environmentObject(TransactionDataStore(MockTransactionRepository()))
|
||||
.environmentObject(TwigsInMemoryCacheService())
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -11,7 +11,7 @@ import TwigsCore
|
|||
|
||||
class CategoryForm: ObservableObject {
|
||||
let category: TwigsCore.Category?
|
||||
let categoryList: CategoryListDataStore
|
||||
let dataStore: DataStore
|
||||
let budgetId: String
|
||||
let categoryId: String
|
||||
@Published var title: String
|
||||
|
@ -23,11 +23,11 @@ class CategoryForm: ObservableObject {
|
|||
|
||||
init(
|
||||
category: TwigsCore.Category?,
|
||||
categoryList: CategoryListDataStore,
|
||||
dataStore: DataStore,
|
||||
budgetId: String
|
||||
) {
|
||||
self.category = category
|
||||
self.categoryList = categoryList
|
||||
self.dataStore = dataStore
|
||||
self.budgetId = budgetId
|
||||
self.categoryId = category?.id ?? ""
|
||||
self.showDelete = !self.categoryId.isEmpty
|
||||
|
@ -42,7 +42,7 @@ class CategoryForm: ObservableObject {
|
|||
|
||||
func save() async {
|
||||
let amount = Int((Double(self.amount) ?? 0.0) * 100)
|
||||
await categoryList.save(TwigsCore.Category(
|
||||
await dataStore.save(TwigsCore.Category(
|
||||
budgetId: budgetId,
|
||||
id: categoryId,
|
||||
title: title,
|
||||
|
@ -57,6 +57,6 @@ class CategoryForm: ObservableObject {
|
|||
guard let category = self.category else {
|
||||
return
|
||||
}
|
||||
await categoryList.delete(category)
|
||||
await dataStore.delete(category)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,13 +10,13 @@ import SwiftUI
|
|||
import TwigsCore
|
||||
|
||||
struct CategoryFormSheet: View {
|
||||
@EnvironmentObject var categoryList: CategoryListDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
@ObservedObject var categoryForm: CategoryForm
|
||||
@State private var showingAlert = false
|
||||
|
||||
@ViewBuilder
|
||||
var stateContent: some View {
|
||||
switch categoryList.category {
|
||||
switch dataStore.category {
|
||||
case .success(_):
|
||||
EmptyView()
|
||||
case .saving(_):
|
||||
|
@ -62,7 +62,7 @@ struct CategoryFormSheet: View {
|
|||
stateContent
|
||||
.navigationBarItems(
|
||||
leading: Button("cancel") {
|
||||
categoryList.cancelEdit()
|
||||
dataStore.cancelEditCategory()
|
||||
},
|
||||
trailing: Button("save") {
|
||||
Task {
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
//
|
||||
// CategoryListDataStore.swift
|
||||
// Budget
|
||||
//
|
||||
// Created by Billy Brawner on 10/1/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import TwigsCore
|
||||
|
||||
@MainActor
|
||||
class CategoryListDataStore: ObservableObject {
|
||||
@Published var categories: AsyncData<[TwigsCore.Category]> = .empty
|
||||
@Published var category: AsyncData<TwigsCore.Category> = .empty {
|
||||
didSet {
|
||||
if case let .success(category) = self.category {
|
||||
self.selectedCategory = category
|
||||
} else if case .empty = self.category {
|
||||
self.selectedCategory = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var selectedCategory: TwigsCore.Category? = nil
|
||||
|
||||
func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = false, count: Int? = nil, page: Int? = nil) async {
|
||||
self.categories = .loading
|
||||
do {
|
||||
let categories = try await categoryRepository.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page)
|
||||
self.categories = .success(categories)
|
||||
} catch {
|
||||
self.categories = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func save(_ category: TwigsCore.Category) async {
|
||||
self.category = .loading
|
||||
do {
|
||||
var savedCategory: TwigsCore.Category
|
||||
if category.id != "" {
|
||||
savedCategory = try await self.categoryRepository.updateCategory(category)
|
||||
} else {
|
||||
savedCategory = try await self.categoryRepository.createCategory(category)
|
||||
}
|
||||
self.category = .success(savedCategory)
|
||||
if case let .success(categories) = self.categories {
|
||||
var updatedCategories = categories.filter(withoutId: category.id)
|
||||
updatedCategories.append(savedCategory)
|
||||
self.categories = .success(updatedCategories.sorted(by: { $0.title < $1.title }))
|
||||
}
|
||||
} catch {
|
||||
self.category = .error(error, category)
|
||||
}
|
||||
}
|
||||
|
||||
func delete(_ category: TwigsCore.Category) async {
|
||||
self.category = .loading
|
||||
do {
|
||||
try await self.categoryRepository.deleteCategory(category.id)
|
||||
self.category = .empty
|
||||
if case let .success(categories) = self.categories {
|
||||
self.categories = .success(categories.filter(withoutId: category.id))
|
||||
}
|
||||
} catch {
|
||||
self.category = .error(error, category)
|
||||
}
|
||||
}
|
||||
|
||||
func edit(_ category: TwigsCore.Category) async {
|
||||
self.category = .editing(category)
|
||||
}
|
||||
|
||||
func cancelEdit() {
|
||||
if let category = self.selectedCategory {
|
||||
self.category = .success(category)
|
||||
} else {
|
||||
self.category = .empty
|
||||
}
|
||||
}
|
||||
|
||||
func clearSelectedCategory() {
|
||||
self.category = .empty
|
||||
}
|
||||
|
||||
private let categoryRepository: CategoryRepository
|
||||
init(_ categoryRepository: CategoryRepository) {
|
||||
self.categoryRepository = categoryRepository
|
||||
}
|
||||
}
|
|
@ -11,26 +11,25 @@ import Combine
|
|||
import TwigsCore
|
||||
|
||||
struct CategoryListView: View {
|
||||
@EnvironmentObject var categoryDataStore: CategoryListDataStore
|
||||
@EnvironmentObject var apiService: TwigsApiService
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
@State var requestId: String = ""
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
InlineLoadingView(
|
||||
data: $categoryDataStore.categories,
|
||||
action: { await self.categoryDataStore.getCategories(budgetId: budget.id, expense: nil, archived: nil, count: nil, page: nil) },
|
||||
data: $dataStore.categories,
|
||||
action: { await self.dataStore.getCategories(budgetId: budget.id, expense: nil, archived: nil, count: nil, page: nil) },
|
||||
errorTextLocalizedStringKey: "Failed to load categories"
|
||||
) { categories in
|
||||
List {
|
||||
Section {
|
||||
ForEach(categories.filter { !$0.archived }) { category in
|
||||
CategoryListItemView(CategoryDataStore(transactionRepository: apiService), budget: budget, category: category)
|
||||
CategoryListItemView(CategoryDataStore(dataStore.apiService), budget: budget, category: category)
|
||||
}
|
||||
}
|
||||
Section("Archived") {
|
||||
ForEach(categories.filter { $0.archived }) { category in
|
||||
CategoryListItemView(CategoryDataStore(transactionRepository: apiService), budget: budget, category: category)
|
||||
CategoryListItemView(CategoryDataStore(dataStore.apiService), budget: budget, category: category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +45,7 @@ struct CategoryListView: View {
|
|||
struct CategoryListItemView: View {
|
||||
let category: TwigsCore.Category
|
||||
let budget: Budget
|
||||
@EnvironmentObject var categoryListDataStore: CategoryListDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
@ObservedObject var categoryDataStore: CategoryDataStore
|
||||
|
||||
init(_ categoryDataStore: CategoryDataStore, budget: Budget, category: TwigsCore.Category) {
|
||||
|
@ -68,11 +67,11 @@ struct CategoryListItemView: View {
|
|||
var body: some View {
|
||||
NavigationLink(
|
||||
tag: category,
|
||||
selection: $categoryListDataStore.selectedCategory,
|
||||
selection: $dataStore.selectedCategory,
|
||||
destination: {
|
||||
CategoryDetailsView(self.budget)
|
||||
.environmentObject(categoryDataStore)
|
||||
.navigationBarTitle(categoryListDataStore.selectedCategory?.title ?? "")
|
||||
.navigationBarTitle(dataStore.selectedCategory?.title ?? "")
|
||||
},
|
||||
label: {
|
||||
VStack(alignment: .leading) {
|
||||
|
|
460
Twigs/DataStore.swift
Normal file
460
Twigs/DataStore.swift
Normal file
|
@ -0,0 +1,460 @@
|
|||
//
|
||||
// DataStore.swift
|
||||
// Twigs
|
||||
//
|
||||
// Created by William Brawner on 5/16/22.
|
||||
// Copyright © 2022 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Collections
|
||||
import Combine
|
||||
import Foundation
|
||||
import TwigsCore
|
||||
|
||||
private let LAST_BUDGET = "LAST_BUDGET"
|
||||
|
||||
@MainActor
|
||||
class DataStore : ObservableObject {
|
||||
let apiService: TwigsApiService
|
||||
@Published var budgets: AsyncData<[Budget]> = .empty
|
||||
@Published var budget: AsyncData<Budget> = .empty {
|
||||
didSet {
|
||||
self.overview = .empty
|
||||
if case let .success(budget) = self.budget {
|
||||
UserDefaults.standard.set(budget.id, forKey: LAST_BUDGET)
|
||||
self.showBudgetSelection = false
|
||||
Task {
|
||||
await loadOverview(budget)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var overview: AsyncData<BudgetOverview> = .empty
|
||||
@Published var showBudgetSelection: Bool = true
|
||||
|
||||
init(
|
||||
_ apiService: TwigsApiService
|
||||
) {
|
||||
self.apiService = apiService
|
||||
self.baseUrl = UserDefaults.standard.string(forKey: KEY_BASE_URL)
|
||||
self.token = UserDefaults.standard.string(forKey: KEY_TOKEN)
|
||||
self.userId = UserDefaults.standard.string(forKey: KEY_USER_ID)
|
||||
}
|
||||
|
||||
func getBudgets(count: Int? = nil, page: Int? = nil) async {
|
||||
// TODO: Find some way to extract this to a generic function
|
||||
self.budgets = .loading
|
||||
do {
|
||||
let budgets = try await self.apiService.getBudgets(count: count, page: page).sorted(by: { $0.name < $1.name })
|
||||
self.budgets = .success(budgets)
|
||||
if self.budget != .empty {
|
||||
return
|
||||
}
|
||||
if let id = UserDefaults.standard.string(forKey: LAST_BUDGET), let lastBudget = budgets.first(where: { $0.id == id }) {
|
||||
self.budget = .success(lastBudget)
|
||||
} else {
|
||||
if let budget = budgets.first {
|
||||
self.budget = .success(budget)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
self.budgets = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func loadOverview(_ budget: Budget) async {
|
||||
self.overview = .loading
|
||||
do {
|
||||
let budgetBalance = try await self.apiService.sumTransactions(budgetId: budget.id, categoryId: nil, from: nil, to: nil)
|
||||
let categories = try await self.apiService.getCategories(budgetId: budget.id, expense: nil, archived: false, count: nil, page: nil)
|
||||
var budgetOverview = BudgetOverview(budget: budget, balance: budgetBalance.balance)
|
||||
try await withThrowingTaskGroup(of: (TwigsCore.Category, BalanceResponse).self) { group in
|
||||
for category in categories {
|
||||
group.addTask {
|
||||
return (category, try await self.apiService.sumTransactions(budgetId: nil, categoryId: category.id, from: nil, to: nil))
|
||||
}
|
||||
}
|
||||
|
||||
for try await (category, response) in group {
|
||||
if category.expense {
|
||||
budgetOverview.expectedExpenses += category.amount
|
||||
} else {
|
||||
budgetOverview.expectedIncome += category.amount
|
||||
}
|
||||
|
||||
if category.expense {
|
||||
budgetOverview.actualExpenses += abs(response.balance)
|
||||
} else {
|
||||
budgetOverview.actualIncome += response.balance
|
||||
}
|
||||
}
|
||||
}
|
||||
self.overview = .success(budgetOverview)
|
||||
} catch {
|
||||
self.overview = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func selectBudget(_ budget: Budget) {
|
||||
self.budget = .success(budget)
|
||||
}
|
||||
|
||||
@Published var categories: AsyncData<[TwigsCore.Category]> = .empty
|
||||
@Published var category: AsyncData<TwigsCore.Category> = .empty {
|
||||
didSet {
|
||||
if case let .success(category) = self.category {
|
||||
self.selectedCategory = category
|
||||
} else if case .empty = self.category {
|
||||
self.selectedCategory = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var selectedCategory: TwigsCore.Category? = nil
|
||||
|
||||
func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = false, count: Int? = nil, page: Int? = nil) async {
|
||||
self.categories = .loading
|
||||
do {
|
||||
let categories = try await apiService.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page)
|
||||
self.categories = .success(categories)
|
||||
} catch {
|
||||
self.categories = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func save(_ category: TwigsCore.Category) async {
|
||||
self.category = .loading
|
||||
do {
|
||||
var savedCategory: TwigsCore.Category
|
||||
if category.id != "" {
|
||||
savedCategory = try await self.apiService.updateCategory(category)
|
||||
} else {
|
||||
savedCategory = try await self.apiService.createCategory(category)
|
||||
}
|
||||
self.category = .success(savedCategory)
|
||||
if case let .success(categories) = self.categories {
|
||||
var updatedCategories = categories.filter(withoutId: category.id)
|
||||
updatedCategories.append(savedCategory)
|
||||
self.categories = .success(updatedCategories.sorted(by: { $0.title < $1.title }))
|
||||
}
|
||||
} catch {
|
||||
self.category = .error(error, category)
|
||||
}
|
||||
}
|
||||
|
||||
func delete(_ category: TwigsCore.Category) async {
|
||||
self.category = .loading
|
||||
do {
|
||||
try await self.apiService.deleteCategory(category.id)
|
||||
self.category = .empty
|
||||
if case let .success(categories) = self.categories {
|
||||
self.categories = .success(categories.filter(withoutId: category.id))
|
||||
}
|
||||
} catch {
|
||||
self.category = .error(error, category)
|
||||
}
|
||||
}
|
||||
|
||||
func edit(_ category: TwigsCore.Category) async {
|
||||
self.category = .editing(category)
|
||||
}
|
||||
|
||||
func cancelEditCategory() {
|
||||
if let category = self.selectedCategory {
|
||||
self.category = .success(category)
|
||||
} else {
|
||||
self.category = .empty
|
||||
}
|
||||
}
|
||||
|
||||
func clearSelectedCategory() {
|
||||
self.category = .empty
|
||||
}
|
||||
|
||||
@Published var recurringTransactions: AsyncData<[RecurringTransaction]> = .empty
|
||||
@Published var recurringTransaction: AsyncData<RecurringTransaction> = .empty {
|
||||
didSet {
|
||||
if case let .success(transaction) = self.recurringTransaction {
|
||||
self.selectedRecurringTransaction = transaction
|
||||
} else if case .empty = recurringTransaction {
|
||||
self.selectedRecurringTransaction = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var selectedRecurringTransaction: RecurringTransaction? = nil
|
||||
|
||||
func getRecurringTransactions() async {
|
||||
guard case let .success(budget) = self.budget else {
|
||||
return
|
||||
}
|
||||
self.recurringTransactions = .loading
|
||||
do {
|
||||
let transactions = try await self.apiService.getRecurringTransactions(budget.id)
|
||||
self.recurringTransactions = .success(transactions.sorted(by: { $0.title < $1.title }))
|
||||
} catch {
|
||||
self.recurringTransactions = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func saveRecurringTransaction(_ transaction: RecurringTransaction) async {
|
||||
self.recurringTransaction = .loading
|
||||
do {
|
||||
var savedTransaction: RecurringTransaction
|
||||
if (transaction.id != "") {
|
||||
savedTransaction = try await self.apiService.updateRecurringTransaction(transaction)
|
||||
} else {
|
||||
savedTransaction = try await self.apiService.createRecurringTransaction(transaction)
|
||||
}
|
||||
self.recurringTransaction = .success(savedTransaction)
|
||||
if case var .success(transactions) = self.recurringTransactions {
|
||||
transactions = transactions.filter(withoutId: savedTransaction.id)
|
||||
transactions.append(savedTransaction)
|
||||
self.recurringTransactions = .success(transactions.sorted(by: { $0.title < $1.title }))
|
||||
}
|
||||
} catch {
|
||||
self.recurringTransactions = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteRecurringTransaction(_ transaction: RecurringTransaction) async {
|
||||
self.recurringTransaction = .loading
|
||||
do {
|
||||
try await self.apiService.deleteRecurringTransaction(transaction.id)
|
||||
self.recurringTransaction = .empty
|
||||
if case let .success(transactions) = self.recurringTransactions {
|
||||
self.recurringTransactions = .success(transactions.filter(withoutId: transaction.id))
|
||||
}
|
||||
} catch {
|
||||
self.recurringTransaction = .error(error, transaction)
|
||||
}
|
||||
}
|
||||
|
||||
func clearSelectedRecurringTransaction() {
|
||||
self.recurringTransaction = .empty
|
||||
}
|
||||
|
||||
@Published var transactions: AsyncData<OrderedDictionary<String, [Transaction]>> = .empty
|
||||
@Published var transaction: AsyncData<Transaction> = .empty {
|
||||
didSet {
|
||||
if case let .success(transaction) = self.transaction {
|
||||
self.selectedTransaction = transaction
|
||||
} else if case .empty = self.transaction {
|
||||
self.selectedTransaction = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var selectedTransaction: Transaction? = nil
|
||||
private var budgetId: String = ""
|
||||
private var categoryId: String? = nil
|
||||
private var from: Date? = nil
|
||||
private var count: Int? = nil
|
||||
private var page: Int? = nil
|
||||
|
||||
func getTransactions(from: Date? = nil, count: Int? = nil, page: Int? = nil) async {
|
||||
self.from = from
|
||||
self.count = count
|
||||
self.page = page
|
||||
await self.getTransactions()
|
||||
}
|
||||
|
||||
func getTransactions() async {
|
||||
guard case let .success(budget) = self.budget else {
|
||||
self.transactions = .error(NetworkError.unknown)
|
||||
return
|
||||
}
|
||||
self.budgetId = budget.id
|
||||
if case let .success(category) = self.category {
|
||||
self.categoryId = category.id
|
||||
} else {
|
||||
self.categoryId = nil
|
||||
}
|
||||
self.transactions = .loading
|
||||
do {
|
||||
var categoryIds: [String] = []
|
||||
if let categoryId = categoryId {
|
||||
categoryIds.append(categoryId)
|
||||
}
|
||||
let transactions = try await self.apiService.getTransactions(
|
||||
budgetIds: [budgetId],
|
||||
categoryIds: categoryIds,
|
||||
from: from ?? Date.firstOfMonth,
|
||||
to: nil,
|
||||
count: count,
|
||||
page: page
|
||||
)
|
||||
let groupedTransactions = OrderedDictionary<String,[Transaction]>(grouping: transactions, by: { $0.date.toLocaleString() })
|
||||
self.transactions = .success(groupedTransactions)
|
||||
} catch {
|
||||
self.transactions = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func saveTransaction(_ transaction: Transaction) async {
|
||||
self.transaction = .saving(transaction)
|
||||
do {
|
||||
var savedTransaction: Transaction
|
||||
if (transaction.id != "") {
|
||||
savedTransaction = try await self.apiService.updateTransaction(transaction)
|
||||
} else {
|
||||
savedTransaction = try await self.apiService.createTransaction(transaction)
|
||||
}
|
||||
self.transaction = .success(savedTransaction)
|
||||
await getTransactions()
|
||||
} catch {
|
||||
self.transaction = .error(error, transaction)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteTransaction(_ transaction: Transaction) async {
|
||||
self.transaction = .loading
|
||||
do {
|
||||
try await self.apiService.deleteTransaction(transaction.id)
|
||||
self.transaction = .empty
|
||||
} catch {
|
||||
self.transaction = .error(error, transaction)
|
||||
}
|
||||
}
|
||||
|
||||
func newTransaction() {
|
||||
var budgetId = ""
|
||||
if case let .success(budget) = self.budget {
|
||||
budgetId = budget.id
|
||||
}
|
||||
var categoryId: String? = nil
|
||||
if case let .success(category) = self.category {
|
||||
categoryId = category.id
|
||||
}
|
||||
var createdBy = ""
|
||||
if case let .success(user) = self.currentUser {
|
||||
createdBy = user.id
|
||||
}
|
||||
self.transaction = .editing(TwigsCore.Transaction(categoryId: categoryId, createdBy: createdBy, budgetId: budgetId))
|
||||
}
|
||||
|
||||
func editTransaction(_ transaction: Transaction) {
|
||||
self.transaction = .editing(transaction)
|
||||
}
|
||||
|
||||
func cancelEditTransaction() {
|
||||
if let transaction = self.selectedTransaction {
|
||||
self.transaction = .success(transaction)
|
||||
} else {
|
||||
self.transaction = .empty
|
||||
}
|
||||
}
|
||||
|
||||
func clearSelectedTransaction() {
|
||||
self.transaction = .empty
|
||||
}
|
||||
|
||||
@Published var currentUser: AsyncData<User> = .empty {
|
||||
didSet {
|
||||
if case .success(_) = self.currentUser {
|
||||
self.showLogin = false
|
||||
} else {
|
||||
self.showLogin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let KEY_BASE_URL = "BASE_URL"
|
||||
private let KEY_TOKEN = "TOKEN"
|
||||
private let KEY_USER_ID = "USER_ID"
|
||||
|
||||
@Published var baseUrl: String? {
|
||||
didSet {
|
||||
self.apiService.baseUrl = baseUrl
|
||||
UserDefaults.standard.set(baseUrl, forKey: KEY_BASE_URL)
|
||||
}
|
||||
}
|
||||
@Published var token: String? {
|
||||
didSet {
|
||||
self.apiService.token = token
|
||||
UserDefaults.standard.set(token, forKey: KEY_TOKEN)
|
||||
}
|
||||
}
|
||||
@Published var userId: String? {
|
||||
didSet {
|
||||
UserDefaults.standard.set(userId, forKey: KEY_USER_ID)
|
||||
}
|
||||
}
|
||||
@Published var showLogin: Bool = true
|
||||
|
||||
func login(server: String, username: String, password: String) async {
|
||||
self.currentUser = .loading
|
||||
self.apiService.baseUrl = server
|
||||
// The API Service applies some validation and correcting of the server before returning it so we use that
|
||||
// value instead of the original one
|
||||
self.baseUrl = self.apiService.baseUrl ?? ""
|
||||
do {
|
||||
let response = try await self.apiService.login(username: username, password: password)
|
||||
self.token = response.token
|
||||
self.userId = response.userId
|
||||
await self.loadProfile()
|
||||
} catch {
|
||||
switch error {
|
||||
case NetworkError.jsonParsingFailed(let jsonError):
|
||||
print(jsonError.localizedDescription)
|
||||
default:
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
self.currentUser = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func register(server: String, username: String, email: String, password: String, confirmPassword: String) async {
|
||||
// TODO: Validate other fields as well
|
||||
if !password.elementsEqual(confirmPassword) {
|
||||
// TODO: Show error message to user
|
||||
return
|
||||
}
|
||||
|
||||
self.apiService.baseUrl = server
|
||||
// The API Service applies some validation and correcting of the server before returning it so we use that
|
||||
// value instead of the original one
|
||||
self.baseUrl = self.apiService.baseUrl ?? ""
|
||||
do {
|
||||
_ = try await apiService.register(username: username, email: email, password: password)
|
||||
} catch {
|
||||
switch error {
|
||||
case NetworkError.jsonParsingFailed(let jsonError):
|
||||
print(jsonError.localizedDescription)
|
||||
default:
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
return
|
||||
}
|
||||
await self.login(server: server, username: username, password: password)
|
||||
}
|
||||
|
||||
func loadProfile() async {
|
||||
guard let userId = self.userId, !userId.isEmpty else {
|
||||
self.currentUser = .error(UserStatus.unauthenticated)
|
||||
return
|
||||
}
|
||||
do {
|
||||
let user = try await self.apiService.getUser(userId)
|
||||
self.currentUser = .success(user)
|
||||
} catch {
|
||||
self.currentUser = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@Published var user: AsyncData<User> = .empty
|
||||
|
||||
func getUser(_ id: String) async {
|
||||
do {
|
||||
let user = try await self.apiService.getUser(id)
|
||||
self.user = .success(user)
|
||||
} catch {
|
||||
self.currentUser = .error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum UserStatus: Error, Equatable {
|
||||
case unauthenticated
|
||||
case authenticating
|
||||
case failedAuthentication
|
||||
case authenticated
|
||||
case passwordMismatch
|
||||
}
|
|
@ -13,7 +13,7 @@ struct LoginView: View {
|
|||
@State var server: String = ""
|
||||
@State var username: String = ""
|
||||
@State var password: String = ""
|
||||
@EnvironmentObject var dataStore: AuthenticationDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
var loading: Bool {
|
||||
switch dataStore.user {
|
||||
case .loading:
|
||||
|
|
|
@ -10,8 +10,9 @@ import SwiftUI
|
|||
import TwigsCore
|
||||
|
||||
struct ProfileView: View {
|
||||
@EnvironmentObject var authDataStore: AuthenticationDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
VStack(spacing: 10) {
|
||||
Image(systemName: "person.circle.fill")
|
||||
|
@ -21,7 +22,9 @@ struct ProfileView: View {
|
|||
.clipShape(Circle())
|
||||
.overlay(Circle().stroke(Color.white, lineWidth: 4))
|
||||
.shadow(radius: 5)
|
||||
Text(authDataStore.currentUser!.username)
|
||||
if case let .success(user) = self.dataStore.currentUser {
|
||||
Text(user.username)
|
||||
}
|
||||
NavigationLink(destination: EmptyView()) {
|
||||
Text("change_password")
|
||||
}
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
//
|
||||
// RecurringTransactionDataStore.swift
|
||||
// Twigs
|
||||
//
|
||||
// Created by William Brawner on 12/6/21.
|
||||
// Copyright © 2021 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import Collections
|
||||
import TwigsCore
|
||||
|
||||
@MainActor
|
||||
class RecurringTransactionDataStore: AsyncObservableObject {
|
||||
private let repository: RecurringTransactionsRepository
|
||||
@Published var transactions: AsyncData<[RecurringTransaction]> = .empty
|
||||
@Published var transaction: AsyncData<RecurringTransaction> = .empty {
|
||||
didSet {
|
||||
if case let .success(transaction) = self.transaction {
|
||||
self.selectedTransaction = transaction
|
||||
} else if case .empty = transaction {
|
||||
self.selectedTransaction = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var selectedTransaction: RecurringTransaction? = nil
|
||||
|
||||
init(_ repository: RecurringTransactionsRepository) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
func getRecurringTransactions(_ budgetId: String) async {
|
||||
self.transactions = .loading
|
||||
do {
|
||||
let transactions = try await self.repository.getRecurringTransactions(budgetId)
|
||||
self.transactions = .success(transactions.sorted(by: { $0.title < $1.title }))
|
||||
} catch {
|
||||
self.transactions = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func saveRecurringTransaction(_ transaction: RecurringTransaction) async {
|
||||
self.transaction = .loading
|
||||
do {
|
||||
var savedTransaction: RecurringTransaction
|
||||
if (transaction.id != "") {
|
||||
savedTransaction = try await self.repository.updateRecurringTransaction(transaction)
|
||||
} else {
|
||||
savedTransaction = try await self.repository.createRecurringTransaction(transaction)
|
||||
}
|
||||
self.transaction = .success(savedTransaction)
|
||||
if case var .success(transactions) = self.transactions {
|
||||
transactions = transactions.filter(withoutId: savedTransaction.id)
|
||||
transactions.append(savedTransaction)
|
||||
self.transactions = .success(transactions.sorted(by: { $0.title < $1.title }))
|
||||
}
|
||||
} catch {
|
||||
self.transactions = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteRecurringTransaction(_ transaction: RecurringTransaction) async {
|
||||
self.transactions = .loading
|
||||
do {
|
||||
try await self.repository.deleteRecurringTransaction(transaction.id)
|
||||
self.transaction = .empty
|
||||
if case let .success(transactions) = self.transactions {
|
||||
self.transactions = .success(transactions.filter(withoutId: transaction.id))
|
||||
}
|
||||
} catch {
|
||||
self.transaction = .error(error, transaction)
|
||||
}
|
||||
}
|
||||
|
||||
func clearSelectedRecurringTransaction() {
|
||||
self.transaction = .empty
|
||||
}
|
||||
}
|
|
@ -10,10 +10,10 @@ import SwiftUI
|
|||
import TwigsCore
|
||||
|
||||
struct RecurringTransactionDetailsView: View {
|
||||
@EnvironmentObject var dataStore: RecurringTransactionDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
|
||||
var body: some View {
|
||||
if let transaction = dataStore.selectedTransaction {
|
||||
if let transaction = dataStore.selectedRecurringTransaction {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text(transaction.title)
|
||||
|
|
|
@ -10,13 +10,12 @@ import SwiftUI
|
|||
import TwigsCore
|
||||
|
||||
struct RecurringTransactionsListView: View {
|
||||
@ObservedObject var dataStore: RecurringTransactionDataStore
|
||||
let budget: Budget
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
|
||||
var body: some View {
|
||||
InlineLoadingView(
|
||||
data: $dataStore.transactions,
|
||||
action: { await self.dataStore.getRecurringTransactions(self.budget.id) },
|
||||
data: $dataStore.recurringTransactions,
|
||||
action: { await self.dataStore.getRecurringTransactions() },
|
||||
errorTextLocalizedStringKey: "Failed to load recurring transactions"
|
||||
) { (transactions: [RecurringTransaction]) in
|
||||
List {
|
||||
|
@ -31,13 +30,13 @@ struct RecurringTransactionsListView: View {
|
|||
#if DEBUG
|
||||
struct RecurringTransactionView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(MockRecurringTransactionRepository()), budget: MockBudgetRepository.budget)
|
||||
RecurringTransactionsListView()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
struct RecurringTransactionsListItemView: View {
|
||||
@EnvironmentObject var dataStore: RecurringTransactionDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
let transaction: RecurringTransaction
|
||||
|
||||
init (_ transaction: RecurringTransaction) {
|
||||
|
@ -47,7 +46,7 @@ struct RecurringTransactionsListItemView: View {
|
|||
var body: some View {
|
||||
NavigationLink(
|
||||
tag: transaction,
|
||||
selection: $dataStore.selectedTransaction,
|
||||
selection: $dataStore.selectedRecurringTransaction,
|
||||
destination: {
|
||||
RecurringTransactionDetailsView()
|
||||
.navigationBarTitle("details", displayMode: .inline)
|
||||
|
|
|
@ -22,7 +22,7 @@ class MockRecurringTransactionRepository: RecurringTransactionsRepository {
|
|||
amount: 10000,
|
||||
categoryId: MockCategoryRepository.category.id,
|
||||
expense: true,
|
||||
createdBy: MockUserRepository.user.id,
|
||||
createdBy: MockUserRepository.currentUser.id,
|
||||
budgetId: MockBudgetRepository.budget.id
|
||||
)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ struct RegistrationView: View {
|
|||
@State var email: String = ""
|
||||
@State var password: String = ""
|
||||
@State var confirmedPassword: String = ""
|
||||
@EnvironmentObject var dataStore: AuthenticationDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
@ -37,7 +37,7 @@ struct RegistrationView: View {
|
|||
.textContentType(UITextContentType.newPassword)
|
||||
Button("action_register", action: {
|
||||
Task {
|
||||
try await self.dataStore.register(
|
||||
await self.dataStore.register(
|
||||
server: self.server,
|
||||
username: self.username,
|
||||
email: self.email,
|
||||
|
|
|
@ -10,16 +10,14 @@ import SwiftUI
|
|||
import TwigsCore
|
||||
|
||||
struct SidebarBudgetView: View {
|
||||
@EnvironmentObject var authenticationDataStore: AuthenticationDataStore
|
||||
@EnvironmentObject var budgetDataStore: BudgetsDataStore
|
||||
@EnvironmentObject var apiService: TwigsApiService
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
@State var isSelectingBudget = true
|
||||
@State var hasSelectedBudget = false
|
||||
@State var tabSelection: Int? = 0
|
||||
|
||||
@ViewBuilder
|
||||
var mainView: some View {
|
||||
if case let .success(budget) = self.budgetDataStore.budget {
|
||||
if case let .success(budget) = self.dataStore.budget {
|
||||
NavigationView {
|
||||
List {
|
||||
NavigationLink(
|
||||
|
@ -33,7 +31,7 @@ struct SidebarBudgetView: View {
|
|||
NavigationLink(
|
||||
tag: 1,
|
||||
selection: $tabSelection,
|
||||
destination: { TransactionListView<EmptyView>(apiService: apiService, budget: budget).navigationBarTitle("transactions") },
|
||||
destination: { TransactionListView<EmptyView>().navigationBarTitle("transactions") },
|
||||
label: { Label("transactions", systemImage: "dollarsign.circle") })
|
||||
.keyboardShortcut("2")
|
||||
NavigationLink(
|
||||
|
@ -45,7 +43,7 @@ struct SidebarBudgetView: View {
|
|||
NavigationLink(
|
||||
tag: 3,
|
||||
selection: $tabSelection,
|
||||
destination: { RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(apiService), budget: budget).navigationBarTitle("recurring_transactions") },
|
||||
destination: { RecurringTransactionsListView().navigationBarTitle("recurring_transactions") },
|
||||
label: { Label("recurring_transactions", systemImage: "arrow.triangle.2.circlepath") })
|
||||
.keyboardShortcut("4")
|
||||
BudgetListsView()
|
||||
|
@ -64,18 +62,18 @@ struct SidebarBudgetView: View {
|
|||
@ViewBuilder
|
||||
var body: some View {
|
||||
mainView
|
||||
.sheet(isPresented: $authenticationDataStore.showLogin,
|
||||
.sheet(isPresented: $dataStore.showLogin,
|
||||
onDismiss: {
|
||||
Task {
|
||||
await self.budgetDataStore.getBudgets()
|
||||
await self.dataStore.getBudgets()
|
||||
}
|
||||
},
|
||||
content: {
|
||||
LoginView()
|
||||
.environmentObject(authenticationDataStore)
|
||||
.environmentObject(dataStore)
|
||||
.onDisappear {
|
||||
Task {
|
||||
await self.budgetDataStore.getBudgets()
|
||||
await self.dataStore.getBudgets()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -10,21 +10,20 @@ import SwiftUI
|
|||
import TwigsCore
|
||||
|
||||
struct TabbedBudgetView: View {
|
||||
@EnvironmentObject var authenticationDataStore: AuthenticationDataStore
|
||||
@EnvironmentObject var budgetDataStore: BudgetsDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
@EnvironmentObject var apiService: TwigsApiService
|
||||
@AppStorage("budget_tab") var tabSelection: Int = 0
|
||||
|
||||
@ViewBuilder
|
||||
var mainView: some View {
|
||||
if case let .success(budget) = budgetDataStore.budget {
|
||||
if case let .success(budget) = dataStore.budget {
|
||||
TabView(selection: $tabSelection) {
|
||||
NavigationView {
|
||||
BudgetDetailsView(budget: budget)
|
||||
.navigationBarTitle("overview")
|
||||
.navigationBarItems(leading: HStack {
|
||||
Button("budgets", action: {
|
||||
self.budgetDataStore.showBudgetSelection = true
|
||||
self.dataStore.showBudgetSelection = true
|
||||
}).padding()
|
||||
})
|
||||
}
|
||||
|
@ -35,7 +34,7 @@ struct TabbedBudgetView: View {
|
|||
.tag(0)
|
||||
.keyboardShortcut("1")
|
||||
NavigationView {
|
||||
TransactionListView<EmptyView>(apiService: apiService, budget: budget)
|
||||
TransactionListView<EmptyView>()
|
||||
.navigationBarTitle("transactions")
|
||||
}
|
||||
.tabItem {
|
||||
|
@ -55,7 +54,7 @@ struct TabbedBudgetView: View {
|
|||
.tag(2)
|
||||
.keyboardShortcut("3")
|
||||
NavigationView {
|
||||
RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(apiService), budget: budget)
|
||||
RecurringTransactionsListView()
|
||||
.navigationBarTitle("recurring_transactions")
|
||||
}
|
||||
.tabItem {
|
||||
|
@ -71,24 +70,24 @@ struct TabbedBudgetView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
mainView.sheet(isPresented: $authenticationDataStore.showLogin,
|
||||
mainView.sheet(isPresented: $dataStore.showLogin,
|
||||
onDismiss: {
|
||||
Task {
|
||||
await self.budgetDataStore.getBudgets()
|
||||
await self.dataStore.getBudgets()
|
||||
}
|
||||
},
|
||||
content: {
|
||||
LoginView()
|
||||
.environmentObject(authenticationDataStore)
|
||||
.environmentObject(dataStore)
|
||||
.onDisappear {
|
||||
Task {
|
||||
await self.budgetDataStore.getBudgets()
|
||||
await self.dataStore.getBudgets()
|
||||
}
|
||||
}
|
||||
}).sheet(isPresented: $budgetDataStore.showBudgetSelection,
|
||||
}).sheet(isPresented: $dataStore.showBudgetSelection,
|
||||
content: {
|
||||
List {
|
||||
BudgetListsView().environmentObject(budgetDataStore)
|
||||
BudgetListsView().environmentObject(dataStore)
|
||||
}
|
||||
})
|
||||
.interactiveDismissDisabled(true)
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
//
|
||||
// TransactionDataStore.swift
|
||||
// Budget
|
||||
//
|
||||
// Created by Billy Brawner on 10/1/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import Collections
|
||||
import TwigsCore
|
||||
|
||||
@MainActor
|
||||
class TransactionDataStore: ObservableObject {
|
||||
@Published var transactions: AsyncData<OrderedDictionary<String, [Transaction]>> = .empty
|
||||
@Published var transaction: AsyncData<Transaction> = .empty {
|
||||
didSet {
|
||||
if case let .success(transaction) = self.transaction {
|
||||
self.selectedTransaction = transaction
|
||||
} else if case .empty = self.transaction {
|
||||
self.selectedTransaction = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var selectedTransaction: Transaction? = nil
|
||||
private var budgetId: String = ""
|
||||
private var categoryId: String? = nil
|
||||
private var from: Date? = nil
|
||||
private var count: Int? = nil
|
||||
private var page: Int? = nil
|
||||
|
||||
func getTransactions(_ budgetId: String, categoryId: String? = nil, from: Date? = nil, count: Int? = nil, page: Int? = nil) async {
|
||||
self.budgetId = budgetId
|
||||
self.categoryId = categoryId
|
||||
self.from = from
|
||||
self.count = count
|
||||
self.page = page
|
||||
await self.getTransactions()
|
||||
}
|
||||
|
||||
func getTransactions() async {
|
||||
self.transactions = .loading
|
||||
do {
|
||||
var categoryIds: [String] = []
|
||||
if let categoryId = categoryId {
|
||||
categoryIds.append(categoryId)
|
||||
}
|
||||
let transactions = try await self.transactionRepository.getTransactions(
|
||||
budgetIds: [budgetId],
|
||||
categoryIds: categoryIds,
|
||||
from: from ?? Date.firstOfMonth,
|
||||
to: nil,
|
||||
count: count,
|
||||
page: page
|
||||
)
|
||||
let groupedTransactions = OrderedDictionary<String,[Transaction]>(grouping: transactions, by: { $0.date.toLocaleString() })
|
||||
self.transactions = .success(groupedTransactions)
|
||||
} catch {
|
||||
self.transactions = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func saveTransaction(_ transaction: Transaction) async {
|
||||
self.transaction = .saving(transaction)
|
||||
do {
|
||||
var savedTransaction: Transaction
|
||||
if (transaction.id != "") {
|
||||
savedTransaction = try await self.transactionRepository.updateTransaction(transaction)
|
||||
} else {
|
||||
savedTransaction = try await self.transactionRepository.createTransaction(transaction)
|
||||
}
|
||||
self.transaction = .success(savedTransaction)
|
||||
await getTransactions()
|
||||
} catch {
|
||||
self.transaction = .error(error, transaction)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteTransaction(_ transaction: Transaction) async {
|
||||
self.transaction = .loading
|
||||
do {
|
||||
try await self.transactionRepository.deleteTransaction(transaction.id)
|
||||
self.transaction = .empty
|
||||
} catch {
|
||||
self.transaction = .error(error, transaction)
|
||||
}
|
||||
}
|
||||
|
||||
func editTransaction(_ transaction: Transaction) {
|
||||
self.transaction = .editing(transaction)
|
||||
}
|
||||
|
||||
func cancelEdit() {
|
||||
if let transaction = self.selectedTransaction {
|
||||
self.transaction = .success(transaction)
|
||||
} else {
|
||||
self.transaction = .empty
|
||||
}
|
||||
}
|
||||
|
||||
func clearSelectedTransaction() {
|
||||
self.transaction = .empty
|
||||
}
|
||||
|
||||
private let transactionRepository: TransactionRepository
|
||||
init(_ transactionRepository: TransactionRepository) {
|
||||
self.transactionRepository = transactionRepository
|
||||
}
|
||||
}
|
|
@ -14,14 +14,10 @@ class TransactionDetails: ObservableObject {
|
|||
@Published var category: AsyncData<TwigsCore.Category> = .empty
|
||||
@Published var budget: AsyncData<Budget> = .empty
|
||||
@Published var user: AsyncData<User> = .empty
|
||||
let budgetRepository: BudgetRepository
|
||||
let categoryRepository: CategoryRepository
|
||||
let userRepository: UserRepository
|
||||
let apiService: TwigsApiService
|
||||
|
||||
init(budgetRepository: BudgetRepository, categoryRepository: CategoryRepository, userRepository: UserRepository) {
|
||||
self.budgetRepository = budgetRepository
|
||||
self.categoryRepository = categoryRepository
|
||||
self.userRepository = userRepository
|
||||
init(_ apiService: TwigsApiService) {
|
||||
self.apiService = apiService
|
||||
}
|
||||
|
||||
func loadDetails(_ transaction: TwigsCore.Transaction) async {
|
||||
|
@ -41,7 +37,7 @@ class TransactionDetails: ObservableObject {
|
|||
private func loadBudget(_ id: String) async {
|
||||
self.budget = .loading
|
||||
do {
|
||||
let budget = try await budgetRepository.getBudget(id)
|
||||
let budget = try await apiService.getBudget(id)
|
||||
self.budget = .success(budget)
|
||||
} catch {
|
||||
self.budget = .error(error)
|
||||
|
@ -51,7 +47,7 @@ class TransactionDetails: ObservableObject {
|
|||
private func loadCategory(_ id: String) async {
|
||||
self.category = .loading
|
||||
do {
|
||||
let category = try await categoryRepository.getCategory(id)
|
||||
let category = try await apiService.getCategory(id)
|
||||
self.category = .success(category)
|
||||
} catch {
|
||||
self.category = .error(error)
|
||||
|
@ -61,7 +57,7 @@ class TransactionDetails: ObservableObject {
|
|||
private func loadUser(_ id: String) async {
|
||||
self.user = .loading
|
||||
do {
|
||||
let user = try await userRepository.getUser(id)
|
||||
let user = try await apiService.getUser(id)
|
||||
self.user = .success(user)
|
||||
} catch {
|
||||
self.user = .error(error)
|
||||
|
|
|
@ -8,12 +8,11 @@
|
|||
|
||||
import SwiftUI
|
||||
import TwigsCore
|
||||
import XCTest
|
||||
import ArgumentParser
|
||||
|
||||
struct TransactionDetailsView: View {
|
||||
@EnvironmentObject var apiService: TwigsApiService
|
||||
@EnvironmentObject var authDataStore: AuthenticationDataStore
|
||||
@EnvironmentObject var dataStore: TransactionDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
@ObservedObject var transactionDetails: TransactionDetails
|
||||
var editing: Bool {
|
||||
if case .editing(_) = dataStore.transaction {
|
||||
|
@ -28,6 +27,15 @@ struct TransactionDetailsView: View {
|
|||
init(_ transactionDetails: TransactionDetails) {
|
||||
self.transactionDetails = transactionDetails
|
||||
}
|
||||
private var currentUserId: String? {
|
||||
get {
|
||||
if case let .success(currentUser) = self.dataStore.currentUser {
|
||||
return currentUser.id
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let transaction = self.dataStore.selectedTransaction {
|
||||
|
@ -63,10 +71,8 @@ struct TransactionDetailsView: View {
|
|||
})
|
||||
.sheet(isPresented: .constant(self.editing), onDismiss: nil) {
|
||||
TransactionFormSheet(transactionForm: TransactionForm(
|
||||
budgetRepository: apiService,
|
||||
categoryRepository: apiService,
|
||||
transactionList: dataStore,
|
||||
createdBy: authDataStore.currentUser!.id,
|
||||
dataStore: dataStore,
|
||||
createdBy: currentUserId!,
|
||||
budgetId: transaction.budgetId,
|
||||
categoryId: transaction.categoryId,
|
||||
transaction: transaction
|
||||
|
@ -173,7 +179,7 @@ struct UserLineItem: View {
|
|||
#if DEBUG
|
||||
struct TransactionDetailsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TransactionDetailsView(TransactionDetails(budgetRepository: MockBudgetRepository(), categoryRepository: MockCategoryRepository(), userRepository: MockUserRepository()))
|
||||
TransactionDetailsView(TransactionDetails(TwigsInMemoryCacheService()))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -10,9 +10,8 @@ import Foundation
|
|||
import TwigsCore
|
||||
|
||||
class TransactionForm: ObservableObject {
|
||||
let budgetRepository: BudgetRepository
|
||||
let categoryRepository: CategoryRepository
|
||||
let transactionList: TransactionDataStore
|
||||
let apiService: TwigsApiService
|
||||
let dataStore: DataStore
|
||||
let transaction: TwigsCore.Transaction?
|
||||
let createdBy: String
|
||||
let transactionId: String
|
||||
|
@ -34,20 +33,17 @@ class TransactionForm: ObservableObject {
|
|||
let showDelete: Bool
|
||||
|
||||
init(
|
||||
budgetRepository: BudgetRepository,
|
||||
categoryRepository: CategoryRepository,
|
||||
transactionList: TransactionDataStore,
|
||||
dataStore: DataStore,
|
||||
createdBy: String,
|
||||
budgetId: String,
|
||||
categoryId: String? = nil,
|
||||
transaction: TwigsCore.Transaction? = nil
|
||||
) {
|
||||
self.budgetRepository = budgetRepository
|
||||
self.categoryRepository = categoryRepository
|
||||
self.apiService = dataStore.apiService
|
||||
self.budgetId = budgetId
|
||||
self.categoryId = categoryId ?? ""
|
||||
self.createdBy = createdBy
|
||||
self.transactionList = transactionList
|
||||
self.dataStore = dataStore
|
||||
let baseTransaction = transaction ?? TwigsCore.Transaction(categoryId: categoryId, createdBy: createdBy, budgetId: budgetId)
|
||||
self.transaction = transaction
|
||||
self.transactionId = baseTransaction.id
|
||||
|
@ -64,13 +60,13 @@ class TransactionForm: ObservableObject {
|
|||
self.categories = .loading
|
||||
var budgets: [Budget]
|
||||
do {
|
||||
budgets = try await budgetRepository.getBudgets(count: nil, page: nil)
|
||||
budgets = try await apiService.getBudgets(count: nil, page: nil)
|
||||
self.budgets = .success(budgets)
|
||||
} catch {
|
||||
self.budgets = .error(error)
|
||||
}
|
||||
do {
|
||||
let categories = try await categoryRepository.getCategories(budgetId: nil, expense: nil, archived: false, count: nil, page: nil)
|
||||
let categories = try await apiService.getCategories(budgetId: nil, expense: nil, archived: false, count: nil, page: nil)
|
||||
self.cachedCategories = categories
|
||||
updateCategories()
|
||||
} catch {
|
||||
|
@ -80,7 +76,7 @@ class TransactionForm: ObservableObject {
|
|||
|
||||
func save() async {
|
||||
let amount = Double(self.amount) ?? 0.0
|
||||
await transactionList.saveTransaction(Transaction(
|
||||
await dataStore.saveTransaction(Transaction(
|
||||
id: transactionId,
|
||||
title: title,
|
||||
description: description,
|
||||
|
@ -97,7 +93,7 @@ class TransactionForm: ObservableObject {
|
|||
guard let transaction = self.transaction else {
|
||||
return
|
||||
}
|
||||
await transactionList.deleteTransaction(transaction)
|
||||
await dataStore.deleteTransaction(transaction)
|
||||
}
|
||||
|
||||
private func updateCategories() {
|
||||
|
|
|
@ -10,14 +10,14 @@ import SwiftUI
|
|||
import TwigsCore
|
||||
|
||||
struct TransactionFormSheet: View {
|
||||
@EnvironmentObject var transactionDataStore: TransactionDataStore
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
@ObservedObject var transactionForm: TransactionForm
|
||||
@State private var showingAlert = false
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
switch self.transactionDataStore.transaction {
|
||||
switch self.dataStore.transaction {
|
||||
case .loading:
|
||||
EmbeddedLoadingView()
|
||||
default:
|
||||
|
@ -63,7 +63,7 @@ struct TransactionFormSheet: View {
|
|||
}
|
||||
.navigationTitle(transactionForm.transactionId.isEmpty ? "add_transaction" : "edit_transaction")
|
||||
.navigationBarItems(
|
||||
leading: Button("cancel", action: { transactionForm.transactionList.cancelEdit() }),
|
||||
leading: Button("cancel", action: { dataStore.cancelEditTransaction() }),
|
||||
trailing: Button("save", action: {
|
||||
Task {
|
||||
await transactionForm.save()
|
||||
|
|
|
@ -12,16 +12,14 @@ import Collections
|
|||
import TwigsCore
|
||||
|
||||
struct TransactionListView<Content>: View where Content: View {
|
||||
@EnvironmentObject var authDataStore: AuthenticationDataStore
|
||||
@StateObject var transactionDataStore: TransactionDataStore
|
||||
let apiService: TwigsApiService
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
@State var search: String = ""
|
||||
@ViewBuilder let header: (() -> Content)?
|
||||
var addingTransaction: Bool {
|
||||
if case .editing(_) = self.transactionDataStore.transaction {
|
||||
if case .editing(_) = self.dataStore.transaction {
|
||||
return true
|
||||
}
|
||||
if case .saving(_) = self.transactionDataStore.transaction {
|
||||
if case .saving(_) = self.dataStore.transaction {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
@ -55,11 +53,41 @@ struct TransactionListView<Content>: View where Content: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var currentUserId: String? {
|
||||
get {
|
||||
if case let .success(currentUser) = dataStore.currentUser {
|
||||
return currentUser.id
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var budgetId: String? {
|
||||
get {
|
||||
if case let .success(budget) = dataStore.budget {
|
||||
return budget.id
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var categoryId: String? {
|
||||
get {
|
||||
if case let .success(category) = dataStore.category {
|
||||
return category.id
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
InlineLoadingView(
|
||||
data: $transactionDataStore.transactions,
|
||||
action: { await transactionDataStore.getTransactions(self.budget.id, categoryId: self.category?.id) },
|
||||
data: $dataStore.transactions,
|
||||
action: { await dataStore.getTransactions() },
|
||||
errorTextLocalizedStringKey: "Failed to load transactions"
|
||||
) { transactions in
|
||||
List {
|
||||
|
@ -67,25 +95,23 @@ struct TransactionListView<Content>: View where Content: View {
|
|||
}
|
||||
.searchable(text: $search)
|
||||
.refreshable {
|
||||
await transactionDataStore.getTransactions(self.budget.id, categoryId: self.category?.id, from: nil, count: nil, page: nil)
|
||||
await dataStore.getTransactions()
|
||||
}
|
||||
.sheet(
|
||||
isPresented: .constant(addingTransaction),
|
||||
content: {
|
||||
TransactionFormSheet(transactionForm: TransactionForm(
|
||||
budgetRepository: apiService,
|
||||
categoryRepository: apiService,
|
||||
transactionList: transactionDataStore,
|
||||
createdBy: authDataStore.currentUser!.id,
|
||||
budgetId: self.budget.id,
|
||||
categoryId: self.category?.id,
|
||||
dataStore: dataStore,
|
||||
createdBy: currentUserId ?? "",
|
||||
budgetId: budgetId ?? "",
|
||||
categoryId: categoryId,
|
||||
transaction: nil
|
||||
))
|
||||
})
|
||||
})
|
||||
.navigationBarItems(
|
||||
trailing: HStack {
|
||||
Button(action: {
|
||||
transactionDataStore.editTransaction(TwigsCore.Transaction(createdBy: authDataStore.currentUser!.id, budgetId: budget.id))
|
||||
dataStore.newTransaction()
|
||||
}) {
|
||||
Image(systemName: "plus")
|
||||
.padding()
|
||||
|
@ -95,20 +121,13 @@ struct TransactionListView<Content>: View where Content: View {
|
|||
}
|
||||
}
|
||||
|
||||
let budget: Budget
|
||||
let category: TwigsCore.Category?
|
||||
init(apiService: TwigsApiService, budget: Budget, category: TwigsCore.Category? = nil, header: (() -> Content)? = nil) {
|
||||
self.apiService = apiService
|
||||
self._transactionDataStore = StateObject(wrappedValue: TransactionDataStore(apiService))
|
||||
self.budget = budget
|
||||
self.category = category
|
||||
init(header: (() -> Content)? = nil) {
|
||||
self.header = header
|
||||
}
|
||||
}
|
||||
|
||||
struct TransactionListItemView: View {
|
||||
@EnvironmentObject var dataStore: TransactionDataStore
|
||||
@EnvironmentObject var apiService: TwigsApiService
|
||||
@EnvironmentObject var dataStore: DataStore
|
||||
var transaction: TwigsCore.Transaction
|
||||
|
||||
var body: some View {
|
||||
|
@ -116,11 +135,8 @@ struct TransactionListItemView: View {
|
|||
tag: self.transaction,
|
||||
selection: self.$dataStore.selectedTransaction,
|
||||
destination: {
|
||||
TransactionDetailsView(TransactionDetails(
|
||||
budgetRepository: apiService,
|
||||
categoryRepository: apiService,
|
||||
userRepository: apiService
|
||||
)).navigationBarTitle("details", displayMode: .inline)
|
||||
TransactionDetailsView(TransactionDetails(dataStore.apiService))
|
||||
.navigationBarTitle("details", displayMode: .inline)
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
|
|
|
@ -20,7 +20,7 @@ class MockTransactionRepository: TransactionRepository {
|
|||
amount: 10000,
|
||||
categoryId: MockCategoryRepository.category.id,
|
||||
expense: true,
|
||||
createdBy: MockUserRepository.user.id,
|
||||
createdBy: MockUserRepository.currentUser.id,
|
||||
budgetId: MockBudgetRepository.budget.id
|
||||
)
|
||||
|
||||
|
|
|
@ -12,20 +12,10 @@ import TwigsCore
|
|||
@main
|
||||
struct TwigsApp: App {
|
||||
@StateObject var apiService: TwigsInMemoryCacheService = TwigsInMemoryCacheService()
|
||||
@AppStorage("BASE_URL") var baseUrl: String = ""
|
||||
@AppStorage("TOKEN") var token: String = ""
|
||||
@AppStorage("USER_ID") var userId: String = ""
|
||||
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
MainView(self.apiService, baseUrl: self.$baseUrl, token: self.$token, userId: self.$userId)
|
||||
.environmentObject(apiService as TwigsApiService)
|
||||
.onAppear {
|
||||
if self.baseUrl != "", self.token != "" {
|
||||
self.apiService.baseUrl = self.baseUrl
|
||||
self.apiService.token = self.token
|
||||
}
|
||||
}
|
||||
MainView(apiService as TwigsApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import TwigsCore
|
||||
|
||||
@MainActor
|
||||
class AuthenticationDataStore: ObservableObject {
|
||||
@Published var user: AsyncData<User> = .empty {
|
||||
didSet {
|
||||
if case let .success(user) = self.user {
|
||||
currentUser = user
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var currentUser: User? = nil {
|
||||
didSet {
|
||||
self.showLogin = self.currentUser == nil
|
||||
}
|
||||
}
|
||||
@Binding private var baseUrl: String
|
||||
@Binding private var token: String
|
||||
@Binding private var userId: String
|
||||
@Published var showLogin: Bool = true
|
||||
let apiService: TwigsApiService
|
||||
|
||||
init(_ apiService: TwigsApiService, baseUrl: Binding<String>, token: Binding<String>, userId: Binding<String>) {
|
||||
self.apiService = apiService
|
||||
self._baseUrl = baseUrl
|
||||
self._token = token
|
||||
self._userId = userId
|
||||
}
|
||||
|
||||
func login(server: String, username: String, password: String) async {
|
||||
self.user = .loading
|
||||
self.apiService.baseUrl = server
|
||||
// The API Service applies some validation and correcting of the server before returning it so we use that
|
||||
// value instead of the original one
|
||||
self.baseUrl = self.apiService.baseUrl ?? ""
|
||||
do {
|
||||
let response = try await self.apiService.login(username: username, password: password)
|
||||
self.token = response.token
|
||||
self.userId = response.userId
|
||||
await self.loadProfile()
|
||||
} catch {
|
||||
switch error {
|
||||
case NetworkError.jsonParsingFailed(let jsonError):
|
||||
print(jsonError.localizedDescription)
|
||||
default:
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
self.user = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func register(server: String, username: String, email: String, password: String, confirmPassword: String) async {
|
||||
// TODO: Validate other fields as well
|
||||
if !password.elementsEqual(confirmPassword) {
|
||||
// TODO: Show error message to user
|
||||
return
|
||||
}
|
||||
|
||||
self.apiService.baseUrl = server
|
||||
// The API Service applies some validation and correcting of the server before returning it so we use that
|
||||
// value instead of the original one
|
||||
self.baseUrl = self.apiService.baseUrl ?? ""
|
||||
do {
|
||||
_ = try await apiService.register(username: username, email: email, password: password)
|
||||
} catch {
|
||||
switch error {
|
||||
case NetworkError.jsonParsingFailed(let jsonError):
|
||||
print(jsonError.localizedDescription)
|
||||
default:
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
return
|
||||
}
|
||||
await self.login(server: server, username: username, password: password)
|
||||
}
|
||||
|
||||
func loadProfile() async {
|
||||
if userId == "" {
|
||||
self.user = .error(UserStatus.unauthenticated)
|
||||
return
|
||||
}
|
||||
do {
|
||||
let user = try await self.apiService.getUser(userId)
|
||||
self.user = .success(user)
|
||||
} catch {
|
||||
self.user = .error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let BASE_URL = "BASE_URL"
|
||||
private let TOKEN = "TOKEN"
|
||||
private let USER_ID = "USER_ID"
|
||||
|
||||
enum UserStatus: Error, Equatable {
|
||||
case unauthenticated
|
||||
case authenticating
|
||||
case failedAuthentication
|
||||
case authenticated
|
||||
case passwordMismatch
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
//
|
||||
// UserDataStore.swift
|
||||
// Twigs
|
||||
//
|
||||
// Created by Billy Brawner on 10/12/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import TwigsCore
|
||||
|
||||
class UserDataStore: AsyncObservableObject {
|
||||
@Published var user: AsyncData<User> = .empty
|
||||
|
||||
func getUser(_ id: String) async {
|
||||
do {
|
||||
let user = try await self.userRepository.getUser(id)
|
||||
self.user = .success(user)
|
||||
} catch {
|
||||
self.user = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
private let userRepository: UserRepository
|
||||
|
||||
init(_ userRepository: UserRepository) {
|
||||
self.userRepository = userRepository
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ import TwigsCore
|
|||
#if DEBUG
|
||||
class MockUserRepository: UserRepository {
|
||||
static let loginResponse = LoginResponse(token: "token", expiration: "2020-01-01T12:00:00Z", userId: "0")
|
||||
static let user = User(id: "0", username: "root", email: "root@localhost", avatar: nil)
|
||||
static let currentUser = User(id: "0", username: "root", email: "root@localhost", avatar: nil)
|
||||
static var token: String? = nil
|
||||
|
||||
func setToken(_ token: String) {
|
||||
|
@ -21,11 +21,11 @@ class MockUserRepository: UserRepository {
|
|||
}
|
||||
|
||||
func getUser(_ id: String) async throws -> User {
|
||||
return MockUserRepository.user
|
||||
return MockUserRepository.currentUser
|
||||
}
|
||||
|
||||
func searchUsers(_ withUsername: String) async throws -> [User] {
|
||||
return [MockUserRepository.user]
|
||||
return [MockUserRepository.currentUser]
|
||||
}
|
||||
|
||||
func setServer(_ server: String) {
|
||||
|
@ -36,7 +36,7 @@ class MockUserRepository: UserRepository {
|
|||
}
|
||||
|
||||
func register(username: String, email: String, password: String) async throws -> User {
|
||||
return MockUserRepository.user
|
||||
return MockUserRepository.currentUser
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,22 +10,12 @@ import SwiftUI
|
|||
import TwigsCore
|
||||
|
||||
struct MainView: View {
|
||||
@StateObject var authenticationDataStore: AuthenticationDataStore
|
||||
@StateObject var budgetDataStore: BudgetsDataStore
|
||||
@StateObject var transactionList: TransactionDataStore
|
||||
@StateObject var categoryList: CategoryListDataStore
|
||||
@StateObject var userDataStore: UserDataStore
|
||||
@StateObject var recurringTransactionList: RecurringTransactionDataStore
|
||||
@StateObject var dataStore: DataStore
|
||||
let apiService: TwigsApiService
|
||||
|
||||
init(_ apiService: TwigsApiService, baseUrl: Binding<String>, token: Binding<String>, userId: Binding<String>) {
|
||||
init(_ apiService: TwigsApiService) {
|
||||
self.apiService = apiService
|
||||
self._authenticationDataStore = StateObject(wrappedValue: AuthenticationDataStore(apiService, baseUrl: baseUrl, token: token, userId: userId))
|
||||
self._budgetDataStore = StateObject(wrappedValue: BudgetsDataStore(budgetRepository: apiService, categoryRepository: apiService, transactionRepository: apiService))
|
||||
self._categoryList = StateObject(wrappedValue: CategoryListDataStore(apiService))
|
||||
self._userDataStore = StateObject(wrappedValue: UserDataStore(apiService))
|
||||
self._transactionList = StateObject(wrappedValue: TransactionDataStore(apiService))
|
||||
self._recurringTransactionList = StateObject(wrappedValue: RecurringTransactionDataStore(apiService))
|
||||
self._dataStore = StateObject(wrappedValue: DataStore(apiService))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -39,15 +29,10 @@ struct MainView: View {
|
|||
|
||||
var body: some View {
|
||||
mainView
|
||||
.environmentObject(transactionList)
|
||||
.environmentObject(categoryList)
|
||||
.environmentObject(budgetDataStore)
|
||||
.environmentObject(userDataStore)
|
||||
.environmentObject(recurringTransactionList)
|
||||
.environmentObject(authenticationDataStore)
|
||||
.environmentObject(dataStore)
|
||||
.onAppear {
|
||||
Task {
|
||||
await self.authenticationDataStore.loadProfile()
|
||||
await self.dataStore.loadProfile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +40,6 @@ struct MainView: View {
|
|||
|
||||
struct MainView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MainView(TwigsInMemoryCacheService(), baseUrl: .constant(""), token: .constant(""), userId: .constant(""))
|
||||
MainView(TwigsInMemoryCacheService())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue