From c3d2ce6270da82130ca9725cec96583257fa29d4 Mon Sep 17 00:00:00 2001 From: Lisheng Guan Date: Sun, 29 Sep 2024 09:12:48 +0800 Subject: [PATCH 1/2] Add Insights Group and refine Currency Edit Picker styple --- MMEX.xcodeproj/project.pbxproj | 10 +++++++++- MMEX/Views/Currencies/CurrencyEditView.swift | 2 +- MMEX/Views/{ => Insights}/InsightsView.swift | 0 3 files changed, 10 insertions(+), 2 deletions(-) rename MMEX/Views/{ => Insights}/InsightsView.swift (100%) diff --git a/MMEX.xcodeproj/project.pbxproj b/MMEX.xcodeproj/project.pbxproj index 6d42562d..b7ca45e2 100644 --- a/MMEX.xcodeproj/project.pbxproj +++ b/MMEX.xcodeproj/project.pbxproj @@ -373,6 +373,14 @@ path = Repositories; sourceTree = ""; }; + A3B51C072CA8E03100BB0681 /* Insights */ = { + isa = PBXGroup; + children = ( + A3C142AD2C9134DD00D3CEC0 /* InsightsView.swift */, + ); + path = Insights; + sourceTree = ""; + }; A3C1421E2C89751500D3CEC0 = { isa = PBXGroup; children = ( @@ -452,6 +460,7 @@ A3C142452C89CB1600D3CEC0 /* Views */ = { isa = PBXGroup; children = ( + A3B51C072CA8E03100BB0681 /* Insights */, A39B1B3E2C9A6714003E5562 /* Settings */, A39B1B3D2C9A66F7003E5562 /* Currencies */, A39B1B392C9A668F003E5562 /* Accounts */, @@ -460,7 +469,6 @@ A39B1B3A2C9A66B8003E5562 /* Payees */, A39B1B3B2C9A66CB003E5562 /* Transactions */, A3C142A72C906E0A00D3CEC0 /* CustomNumberPadView.swift */, - A3C142AD2C9134DD00D3CEC0 /* InsightsView.swift */, A37E7D832C9AC14800B4ECFC /* ManagementView.swift */, ); path = Views; diff --git a/MMEX/Views/Currencies/CurrencyEditView.swift b/MMEX/Views/Currencies/CurrencyEditView.swift index 83f587ec..f0360a79 100644 --- a/MMEX/Views/Currencies/CurrencyEditView.swift +++ b/MMEX/Views/Currencies/CurrencyEditView.swift @@ -40,7 +40,7 @@ struct CurrencyEditView: View { } } .labelsHidden() - .pickerStyle(MenuPickerStyle()) // Adjust the style of the picker as needed + .pickerStyle(SegmentedPickerStyle()) // Adjust the style of the picker as needed } } } diff --git a/MMEX/Views/InsightsView.swift b/MMEX/Views/Insights/InsightsView.swift similarity index 100% rename from MMEX/Views/InsightsView.swift rename to MMEX/Views/Insights/InsightsView.swift From a7e7687716c6797f03aec081a8d2127dc093919d Mon Sep 17 00:00:00 2001 From: Lisheng Guan Date: Sun, 29 Sep 2024 11:34:29 +0800 Subject: [PATCH 2/2] Add Insights group and split into piece, use dataManager to fetch basci stats in Settings --- MMEX.xcodeproj/project.pbxproj | 8 ++++ MMEX/DatabaseManager.swift | 8 ++++ MMEX/Models/TransactionData.swift | 7 +++ MMEX/ViewModels/InfotableViewModel.swift | 6 --- MMEX/ViewModels/InsightsViewModel.swift | 25 ++++++++-- MMEX/Views/Insights/IncomeAndExpense.swift | 53 ++++++++++++++++++++++ MMEX/Views/Insights/InsightsView.swift | 46 ++++--------------- MMEX/Views/Insights/Summary.swift | 31 +++++++++++++ MMEX/Views/Settings/SettingsView.swift | 4 +- 9 files changed, 138 insertions(+), 50 deletions(-) create mode 100644 MMEX/Views/Insights/IncomeAndExpense.swift create mode 100644 MMEX/Views/Insights/Summary.swift diff --git a/MMEX.xcodeproj/project.pbxproj b/MMEX.xcodeproj/project.pbxproj index b7ca45e2..1fc4ac31 100644 --- a/MMEX.xcodeproj/project.pbxproj +++ b/MMEX.xcodeproj/project.pbxproj @@ -53,6 +53,8 @@ A3363EE72C93249D004696C7 /* CategoryAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3363EE62C93249D004696C7 /* CategoryAddView.swift */; }; A3363EE92C9326A1004696C7 /* CategoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3363EE82C9326A1004696C7 /* CategoryListView.swift */; }; A3363EEB2C93BF62004696C7 /* CurrencyRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3363EEA2C93BF62004696C7 /* CurrencyRepository.swift */; }; + A33742882CA8E55400698466 /* IncomeAndExpense.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33742872CA8E55400698466 /* IncomeAndExpense.swift */; }; + A337428A2CA8E72C00698466 /* Summary.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33742892CA8E72C00698466 /* Summary.swift */; }; A3462F642C9426F500F79145 /* InsightsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3462F632C9426F500F79145 /* InsightsViewModel.swift */; }; A3462F662C94854800F79145 /* ExportableEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3462F652C94854800F79145 /* ExportableEntity.swift */; }; A3462F6A2C948CDB00F79145 /* ExportableEntityDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3462F692C948CDB00F79145 /* ExportableEntityDocument.swift */; }; @@ -156,6 +158,8 @@ A3363EE62C93249D004696C7 /* CategoryAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryAddView.swift; sourceTree = ""; }; A3363EE82C9326A1004696C7 /* CategoryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryListView.swift; sourceTree = ""; }; A3363EEA2C93BF62004696C7 /* CurrencyRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyRepository.swift; sourceTree = ""; }; + A33742872CA8E55400698466 /* IncomeAndExpense.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomeAndExpense.swift; sourceTree = ""; }; + A33742892CA8E72C00698466 /* Summary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Summary.swift; sourceTree = ""; }; A3462F632C9426F500F79145 /* InsightsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsightsViewModel.swift; sourceTree = ""; }; A3462F652C94854800F79145 /* ExportableEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportableEntity.swift; sourceTree = ""; }; A3462F692C948CDB00F79145 /* ExportableEntityDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportableEntityDocument.swift; sourceTree = ""; }; @@ -377,6 +381,8 @@ isa = PBXGroup; children = ( A3C142AD2C9134DD00D3CEC0 /* InsightsView.swift */, + A33742872CA8E55400698466 /* IncomeAndExpense.swift */, + A33742892CA8E72C00698466 /* Summary.swift */, ); path = Insights; sourceTree = ""; @@ -576,6 +582,7 @@ 929EF6652C9FF3FB0051A3E6 /* StockRepository.swift in Sources */, A3C142502C8B366400D3CEC0 /* PayeeListView.swift in Sources */, A379C1A72CA3B79E00CC8E2C /* AssetAddView.swift in Sources */, + A337428A2CA8E72C00698466 /* Summary.swift in Sources */, A3C142672C8F2AF500D3CEC0 /* TransactionData.swift in Sources */, 920824042CA4ADC100388AB2 /* ScheduledSplitData.swift in Sources */, A3462F642C9426F500F79145 /* InsightsViewModel.swift in Sources */, @@ -622,6 +629,7 @@ A3462F6A2C948CDB00F79145 /* ExportableEntityDocument.swift in Sources */, 920824122CA4C6A400388AB2 /* BudgetTableRepository.swift in Sources */, A3C142982C8FE62200D3CEC0 /* TransactionListView.swift in Sources */, + A33742882CA8E55400698466 /* IncomeAndExpense.swift in Sources */, A3C142542C8B381400D3CEC0 /* PayeeEditView.swift in Sources */, 920823F82CA498B500388AB2 /* CurrencyHistoryRepository.swift in Sources */, A37E7D8A2C9AC30700B4ECFC /* HelpFAQView.swift in Sources */, diff --git a/MMEX/DatabaseManager.swift b/MMEX/DatabaseManager.swift index 8542ab23..21a1c923 100644 --- a/MMEX/DatabaseManager.swift +++ b/MMEX/DatabaseManager.swift @@ -104,6 +104,14 @@ extension DataManager { currencyFormat = [:] print("Database connection closed.") } + + /// basic stats + func getDatabaseFileName() -> String? { + return self.databaseURL?.lastPathComponent + } + func getDatabaseUserVersion() -> Int32? { + return self.db?.userVersion + } } extension DataManager { diff --git a/MMEX/Models/TransactionData.swift b/MMEX/Models/TransactionData.swift index 7fac50d3..81a76e76 100644 --- a/MMEX/Models/TransactionData.swift +++ b/MMEX/Models/TransactionData.swift @@ -118,6 +118,13 @@ extension TransactionData { } return 0.0 } + var actual: Double { + return switch transCode { + case .withdrawal: 0 - transAmount; + case .deposit: transAmount; + default: 0.0 + } + } var transfer: Double { // TODO: in base currency if transCode == .transfer { diff --git a/MMEX/ViewModels/InfotableViewModel.swift b/MMEX/ViewModels/InfotableViewModel.swift index 57b1b706..fb230a4a 100644 --- a/MMEX/ViewModels/InfotableViewModel.swift +++ b/MMEX/ViewModels/InfotableViewModel.swift @@ -62,12 +62,6 @@ class InfotableViewModel: ObservableObject { ) } } - - var userVersion: Int32 { self.dataManager.repository?.userVersion ?? 0 } - - func getDatabaseURL() -> URL { - return self.dataManager.databaseURL! - } // Set up individual bindings for each @Published property private func setupBindings() { diff --git a/MMEX/ViewModels/InsightsViewModel.swift b/MMEX/ViewModels/InsightsViewModel.swift index cbe17c1b..2a8e3ab5 100644 --- a/MMEX/ViewModels/InsightsViewModel.swift +++ b/MMEX/ViewModels/InsightsViewModel.swift @@ -12,8 +12,10 @@ import Combine class InsightsViewModel: ObservableObject { private var dataManager: DataManager + + @Published var stats: [TransactionData] = [] // all transactions // Published properties for the view to observe - @Published var stats: [TransactionData] = [] + @Published var recentStats: [TransactionData] = [] @Published var startDate: Date @Published var endDate: Date @@ -25,18 +27,19 @@ class InsightsViewModel: ObservableObject { self.endDate = Date() // Load transactions on initialization + loadRecentTransactions() loadTransactions() // Automatically reload transactions when date range changes $startDate .combineLatest($endDate) .sink { [weak self] startDate, endDate in - self?.loadTransactions() + self?.loadRecentTransactions() } .store(in: &cancellables) } - func loadTransactions() { + func loadRecentTransactions() { let repository = dataManager.transactionRepository // Fetch transactions asynchronously @@ -45,7 +48,21 @@ class InsightsViewModel: ObservableObject { // Update the published stats on the main thread DispatchQueue.main.async { - self.stats = transactions + self.recentStats = transactions + } + } + } + + func loadTransactions() { + if let repository = dataManager.transactionRepository { + // Fetch transactions asynchronously + DispatchQueue.global(qos: .background).async { + let transactions = repository.load() + + // Update the published stats on the main thread + DispatchQueue.main.async { + self.stats = transactions + } } } } diff --git a/MMEX/Views/Insights/IncomeAndExpense.swift b/MMEX/Views/Insights/IncomeAndExpense.swift new file mode 100644 index 00000000..0083ebce --- /dev/null +++ b/MMEX/Views/Insights/IncomeAndExpense.swift @@ -0,0 +1,53 @@ +// +// IncomeAndExpense.swift +// MMEX +// +// Created by Lisheng Guan on 2024/9/29. +// + +import SwiftUI +import Charts + +struct IncomeAndExpense: View { + @Binding var stats: [TransactionData] + var body: some View { + Chart() { + ForEach(stats) { stat in + Plot { + BarMark( + x: .value("Day", stat.day), + y: .value("Amount", stat.income) + ) + // .foregroundStyle(by: .value("Status", $0.status.fullName)) + .foregroundStyle(.green) + } + .accessibilityLabel("income") + .accessibilityValue("\(stat.income)") + } + + ForEach(stats) { stat in + Plot { + BarMark( + x: .value("Day", stat.day), + y: .value("Amount", 0 - stat.expenses) + ) + // .foregroundStyle(by: .value("Status", $0.status.fullName)) + .foregroundStyle(.purple) + } + .accessibilityLabel("expenses") + .accessibilityValue("\(stat.expenses)") + } + } + .chartYAxis { + AxisMarks(preset: .automatic, position: .leading) + } + .frame(height: 300) + .chartYAxis(.automatic) + .chartXAxis(.automatic) + .padding(.horizontal) + } +} + +#Preview { + IncomeAndExpense(stats: .constant(TransactionData.sampleData)) +} diff --git a/MMEX/Views/Insights/InsightsView.swift b/MMEX/Views/Insights/InsightsView.swift index cc4d6df4..53eef46f 100644 --- a/MMEX/Views/Insights/InsightsView.swift +++ b/MMEX/Views/Insights/InsightsView.swift @@ -15,6 +15,13 @@ struct InsightsView: View { NavigationStack { ScrollView { VStack(spacing: 20) { + Section { + Summary(stats: $viewModel.stats) + } header: { + Text("Account Income Summary") + .font(.headline) + .padding(.horizontal) + } // Date Range Filters Section Section { @@ -36,7 +43,7 @@ struct InsightsView: View { } Section { - incomeVSexpense + IncomeAndExpense(stats: $viewModel.recentStats) } header: { Text("Income vs Expense Over Time") .font(.headline) @@ -68,43 +75,6 @@ struct InsightsView: View { .navigationBarTitleDisplayMode(.inline) // Ensure title is inline to reduce top space } } - - private var incomeVSexpense: some View { - Chart() { - ForEach(viewModel.stats) { stat in - Plot { - BarMark( - x: .value("Day", stat.day), - y: .value("Amount", stat.income) - ) - // .foregroundStyle(by: .value("Status", $0.status.fullName)) - .foregroundStyle(.green) - } - .accessibilityLabel("income") - .accessibilityValue("\(stat.income)") - } - - ForEach(viewModel.stats) { stat in - Plot { - BarMark( - x: .value("Day", stat.day), - y: .value("Amount", 0 - stat.expenses) - ) - // .foregroundStyle(by: .value("Status", $0.status.fullName)) - .foregroundStyle(.purple) - } - .accessibilityLabel("expenses") - .accessibilityValue("\(stat.expenses)") - } - } - .chartYAxis { - AxisMarks(preset: .automatic, position: .leading) - } - .frame(height: 300) - .chartYAxis(.automatic) - .chartXAxis(.automatic) - .padding(.horizontal) - } } #Preview { diff --git a/MMEX/Views/Insights/Summary.swift b/MMEX/Views/Insights/Summary.swift new file mode 100644 index 00000000..9f0c2299 --- /dev/null +++ b/MMEX/Views/Insights/Summary.swift @@ -0,0 +1,31 @@ +// +// Summary.swift +// MMEX +// +// Created by Lisheng Guan on 2024/9/29. +// + +import SwiftUI +import Charts + +struct Summary: View { + @Binding var stats: [TransactionData] + + var body: some View { + Chart(stats) { + BarMark( + x: .value("Amount", $0.income), + y: .value("Account", String($0.accountId)) + ) + .foregroundStyle(by: .value("Status", $0.status.fullName)) + } + .chartXAxis (.automatic) + .chartYAxis (.automatic) + .frame(maxHeight: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/) + .frame(idealHeight: 300) + } +} + +#Preview { + Summary(stats: .constant(TransactionData.sampleData)) +} diff --git a/MMEX/Views/Settings/SettingsView.swift b/MMEX/Views/Settings/SettingsView.swift index 7d085568..37e70dfc 100644 --- a/MMEX/Views/Settings/SettingsView.swift +++ b/MMEX/Views/Settings/SettingsView.swift @@ -60,12 +60,12 @@ struct SettingsView: View { HStack { Text("Database File") Spacer() - Text(viewModel.getDatabaseURL().lastPathComponent) + Text(dataManager.getDatabaseFileName() ?? "") } HStack { Text("Schema Version") Spacer() - Text("\(viewModel.userVersion)") + Text(String(dataManager.getDatabaseUserVersion() ?? 0)) } HStack { Text("Date Format")