From 8aaf5cc9375761e4527c5838434dee1556256152 Mon Sep 17 00:00:00 2001 From: George Ef Date: Sun, 29 Sep 2024 23:53:51 +0200 Subject: [PATCH 1/5] add InsightsAccountView --- MMEX.xcodeproj/project.pbxproj | 20 +-- MMEX/DatabaseManager.swift | 2 +- MMEX/Models/AccountData.swift | 22 ++++ MMEX/Models/TransactionData.swift | 4 + MMEX/Repositories/AccountRepository.swift | 119 +++++++++++++++++- MMEX/Repositories/CurrencyRepository.swift | 6 +- MMEX/Repositories/Repository.swift | 4 +- MMEX/Repositories/RepositoryProtocol.swift | 2 +- MMEX/ViewModels/InsightsViewModel.swift | 76 ++++++++--- ...dExpense.swift => IncomeExpenseView.swift} | 5 +- MMEX/Views/Insights/InsightsAccountView.swift | 88 +++++++++++++ .../{Summary.swift => InsightsSummary.swift} | 4 +- MMEX/Views/Insights/InsightsView.swift | 12 +- 13 files changed, 323 insertions(+), 41 deletions(-) rename MMEX/Views/Insights/{IncomeAndExpense.swift => IncomeExpenseView.swift} (93%) create mode 100644 MMEX/Views/Insights/InsightsAccountView.swift rename MMEX/Views/Insights/{Summary.swift => InsightsSummary.swift} (85%) diff --git a/MMEX.xcodeproj/project.pbxproj b/MMEX.xcodeproj/project.pbxproj index 1fc4ac31..de112629 100644 --- a/MMEX.xcodeproj/project.pbxproj +++ b/MMEX.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ 9208242C2CA615B400388AB2 /* FieldRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9208242B2CA615B400388AB2 /* FieldRepository.swift */; }; 9208242E2CA617FB00388AB2 /* FieldContentRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9208242D2CA617FB00388AB2 /* FieldContentRepository.swift */; }; 920824322CA6E32800388AB2 /* RepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920824312CA6E32800388AB2 /* RepositoryProtocol.swift */; }; + 924352E02CA9CC5A0052E4BC /* InsightsAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924352DF2CA9CC5A0052E4BC /* InsightsAccountView.swift */; }; 929EF65F2C9FF2DE0051A3E6 /* AssetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 929EF65E2C9FF2DE0051A3E6 /* AssetData.swift */; }; 929EF6612C9FF2FD0051A3E6 /* StockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 929EF6602C9FF2FD0051A3E6 /* StockData.swift */; }; 929EF6632C9FF3ED0051A3E6 /* AssetRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 929EF6622C9FF3ED0051A3E6 /* AssetRepository.swift */; }; @@ -53,8 +54,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 */; }; + A33742882CA8E55400698466 /* IncomeExpenseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33742872CA8E55400698466 /* IncomeExpenseView.swift */; }; + A337428A2CA8E72C00698466 /* InsightsSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33742892CA8E72C00698466 /* InsightsSummary.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 */; }; @@ -142,6 +143,7 @@ 9208242B2CA615B400388AB2 /* FieldRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FieldRepository.swift; sourceTree = ""; }; 9208242D2CA617FB00388AB2 /* FieldContentRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FieldContentRepository.swift; sourceTree = ""; }; 920824312CA6E32800388AB2 /* RepositoryProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryProtocol.swift; sourceTree = ""; }; + 924352DF2CA9CC5A0052E4BC /* InsightsAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsightsAccountView.swift; sourceTree = ""; }; 929EF65E2C9FF2DE0051A3E6 /* AssetData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetData.swift; sourceTree = ""; }; 929EF6602C9FF2FD0051A3E6 /* StockData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StockData.swift; sourceTree = ""; }; 929EF6622C9FF3ED0051A3E6 /* AssetRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetRepository.swift; sourceTree = ""; }; @@ -158,8 +160,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 = ""; }; + A33742872CA8E55400698466 /* IncomeExpenseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomeExpenseView.swift; sourceTree = ""; }; + A33742892CA8E72C00698466 /* InsightsSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsightsSummary.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 = ""; }; @@ -381,8 +383,9 @@ isa = PBXGroup; children = ( A3C142AD2C9134DD00D3CEC0 /* InsightsView.swift */, - A33742872CA8E55400698466 /* IncomeAndExpense.swift */, - A33742892CA8E72C00698466 /* Summary.swift */, + 924352DF2CA9CC5A0052E4BC /* InsightsAccountView.swift */, + A33742892CA8E72C00698466 /* InsightsSummary.swift */, + A33742872CA8E55400698466 /* IncomeExpenseView.swift */, ); path = Insights; sourceTree = ""; @@ -582,7 +585,7 @@ 929EF6652C9FF3FB0051A3E6 /* StockRepository.swift in Sources */, A3C142502C8B366400D3CEC0 /* PayeeListView.swift in Sources */, A379C1A72CA3B79E00CC8E2C /* AssetAddView.swift in Sources */, - A337428A2CA8E72C00698466 /* Summary.swift in Sources */, + A337428A2CA8E72C00698466 /* InsightsSummary.swift in Sources */, A3C142672C8F2AF500D3CEC0 /* TransactionData.swift in Sources */, 920824042CA4ADC100388AB2 /* ScheduledSplitData.swift in Sources */, A3462F642C9426F500F79145 /* InsightsViewModel.swift in Sources */, @@ -590,6 +593,7 @@ A3C142962C8FE15E00D3CEC0 /* TransactionRepository.swift in Sources */, A39B1B342C99A0A8003E5562 /* CurrencyDetailView.swift in Sources */, A3C1422D2C89751500D3CEC0 /* ContentView.swift in Sources */, + 924352E02CA9CC5A0052E4BC /* InsightsAccountView.swift in Sources */, 929EF65F2C9FF2DE0051A3E6 /* AssetData.swift in Sources */, 920823FE2CA4A3F700388AB2 /* TagData.swift in Sources */, A379C1A92CA3B9EB00CC8E2C /* AssetListView.swift in Sources */, @@ -629,7 +633,7 @@ A3462F6A2C948CDB00F79145 /* ExportableEntityDocument.swift in Sources */, 920824122CA4C6A400388AB2 /* BudgetTableRepository.swift in Sources */, A3C142982C8FE62200D3CEC0 /* TransactionListView.swift in Sources */, - A33742882CA8E55400698466 /* IncomeAndExpense.swift in Sources */, + A33742882CA8E55400698466 /* IncomeExpenseView.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 21a1c923..daca3477 100644 --- a/MMEX/DatabaseManager.swift +++ b/MMEX/DatabaseManager.swift @@ -126,7 +126,7 @@ extension DataManager { extension DataManager { func loadCurrencyFormat() { - currencyFormat = CurrencyRepository(db)?.dictionaryRefFormat() ?? [:] + currencyFormat = CurrencyRepository(db)?.dictRefFormat() ?? [:] } func updateCurrencyFormat(id: Int64, value: CurrencyFormat) { diff --git a/MMEX/Models/AccountData.swift b/MMEX/Models/AccountData.swift index a0e5da0b..48241e18 100644 --- a/MMEX/Models/AccountData.swift +++ b/MMEX/Models/AccountData.swift @@ -80,6 +80,28 @@ extension AccountData: DataProtocol { } } +struct AccountFlow { + let inflow : Double + let outflow : Double +} + +extension AccountFlow { + var diff: Double { inflow - outflow } +} + +typealias AccountFlowByStatus = [TransactionStatus: AccountFlow] + +extension AccountFlowByStatus { + var diffVoid : Double { self[.void]? .diff ?? 0.0 } + var diffReconciled : Double { self[.reconciled]? .diff ?? 0.0 } + var diffTotal : Double { + (self[.none]? .diff ?? 0.0) + + (self[.reconciled]? .diff ?? 0.0) + + (self[.followUp]? .diff ?? 0.0) + + (self[.duplicate]? .diff ?? 0.0) + } +} + extension AccountData { static let sampleData : [AccountData] = [ AccountData( diff --git a/MMEX/Models/TransactionData.swift b/MMEX/Models/TransactionData.swift index 81a76e76..c94eeddc 100644 --- a/MMEX/Models/TransactionData.swift +++ b/MMEX/Models/TransactionData.swift @@ -104,6 +104,7 @@ extension TransactionData { } return transDate // If parsing fails, return original string } + var income: Double { // TODO: in base currency if transCode == .deposit { @@ -111,6 +112,7 @@ extension TransactionData { } return 0.0 } + var expenses: Double { // TODO: in base currency if transCode == .withdrawal { @@ -118,6 +120,7 @@ extension TransactionData { } return 0.0 } + var actual: Double { return switch transCode { case .withdrawal: 0 - transAmount; @@ -125,6 +128,7 @@ extension TransactionData { default: 0.0 } } + var transfer: Double { // TODO: in base currency if transCode == .transfer { diff --git a/MMEX/Repositories/AccountRepository.swift b/MMEX/Repositories/AccountRepository.swift index 6bd7cd53..5c9a0e6c 100644 --- a/MMEX/Repositories/AccountRepository.swift +++ b/MMEX/Repositories/AccountRepository.swift @@ -156,16 +156,131 @@ struct AccountRepository: RepositoryProtocol { } extension AccountRepository { - // load all accounts + // load all accounts, sorted by name func load() -> [AccountData] { return select(from: Self.table .order(Self.col_name) ) } + // select accounts by type + func selectByType( + from table: SQLite.Table = Self.table + ) -> [AccountType: [AccountData]] { + do { + var dataByType: [AccountType: [AccountData]] = [:] + for row in try db.prepare(Self.selectData(from: table)) { + let type = AccountType(collateNoCase: row[Self.col_type]) + if dataByType[type] == nil { dataByType[type] = [] } + dataByType[type]!.append(Self.fetchData(row)) + } + print("Successfull select from \(Self.repositoryName): \(dataByType.count)") + return dataByType + } catch { + print("Failed select from \(Self.repositoryName): \(error)") + return [:] + } + } + + // load account flow, indexed by id and transaction status + func dictFlowByStatus( + from table: SQLite.Table = Self.table, + minDate: String? = nil, + maxDate: String? = nil + ) -> [Int64: AccountFlowByStatus] { + let minDate = minDate ?? "" + let supDate = (maxDate ?? "") + "z" + + typealias T = TransactionRepository + let B_query = T.table.select( + T.col_accountId, + T.col_status, + T.col_transAmount, 0 + ) + .where( + T.col_transCode == "Deposit" && + T.col_transDate ?? "" >= minDate && + T.col_transDate ?? "" < supDate && + T.col_deletedTime ?? "" == "" + ) + .union(all: true, T.table.select( + T.col_accountId, + T.col_status, + 0, T.col_transAmount + ) + .where( + T.col_transCode == "Withdrawal" && + T.col_transDate ?? "" >= minDate && + T.col_transDate ?? "" < supDate && + T.col_deletedTime ?? "" == "" + ) + ).union(all: true, T.table.select( + T.col_accountId, + T.col_status, + 0, T.col_transAmount + ) + .where( + T.col_transCode == "Transfer" && + T.col_transDate ?? "" >= minDate && + T.col_transDate ?? "" < supDate && + T.col_deletedTime ?? "" == "" + ) + ).union(all: true, T.table.select( + T.col_toAccountId, + T.col_status, + T.col_toTransAmount, 0 + ) + .where( + T.col_transCode == "Transfer" && + T.col_transDate ?? "" >= minDate && + T.col_transDate ?? "" < supDate && + T.col_deletedTime ?? "" == "" + ) + ) + + typealias A = Self + let B_table = SQLite.Table("b") + let B_col_inflow = SQLite.Expression("INFLOW") + let B_col_outflow = SQLite.Expression("OUTFLOW") + let query = table.with( + B_table, + columns: [A.col_id, T.col_status, B_col_inflow, B_col_outflow], + recursive: false, + as: B_query + ) + .join(B_table, on: B_table[A.col_id] == A.table[A.col_id]) + .where(A.table[A.col_type] != "Investment") + .select( + A.table[A.col_id], + B_table[T.col_status], + B_table[B_col_inflow].total, + B_table[B_col_outflow].total + ) + .group(A.table[A.col_id], B_table[T.col_status]) + + print("DEBUG: AccountRepository.dictFlowByStatus: \(query.expression.description)") + do { + var dict: [Int64: AccountFlowByStatus] = [:] + for row in try db.prepare(query) { + let id = row[A.table[A.col_id]] + let status = TransactionStatus(collateNoCase: row[B_table[T.col_status]]) + if dict[id] == nil { dict[id] = [:] } + dict[id]![status] = AccountFlow( + inflow : row[B_table[B_col_inflow].total], + outflow : row[B_table[B_col_outflow].total] + ) + } + print("Successfull dictionary from \(Self.repositoryName): \(dict.count)") + return dict + } catch { + print("Failed dictionary from \(Self.repositoryName): \(error)") + return [:] + } + } + // load currencyId for all accounts func loadCurrencyId() -> [Int64] { - return Repository(db).select(from: Self.table + return select(from: Self.table .select(distinct: Self.col_currencyId) ) { row in row[Self.col_currencyId] diff --git a/MMEX/Repositories/CurrencyRepository.swift b/MMEX/Repositories/CurrencyRepository.swift index b3e35df4..23a09046 100644 --- a/MMEX/Repositories/CurrencyRepository.swift +++ b/MMEX/Repositories/CurrencyRepository.swift @@ -162,8 +162,8 @@ extension CurrencyRepository { // TODO: re-write in a more readable way (get the ids first, then pluck each currency) // load all referred currency formats, indexed by currencyId - func dictionaryRefFormat() -> [Int64: CurrencyFormat] { - print("DEBUG: CurrencyRepository.dictionaryRefFormat()") + func dictRefFormat() -> [Int64: CurrencyFormat] { + print("DEBUG: CurrencyRepository.dictRefFormat()") typealias C = CurrencyRepository typealias A = AccountRepository typealias E = AssetRepository @@ -182,7 +182,7 @@ extension CurrencyRepository { " union " + "select 1 from \(E.repositoryName) where \(E.table[E.col_currencyId]) == \(C.table[C.col_id])" + ")" - return Repository(db).dictionary( + return Repository(db).dict( query: query ) { row in CurrencyFormat( name : row[1] as? String ?? "", diff --git a/MMEX/Repositories/Repository.swift b/MMEX/Repositories/Repository.swift index 959f1861..d31e8ac4 100644 --- a/MMEX/Repositories/Repository.swift +++ b/MMEX/Repositories/Repository.swift @@ -53,11 +53,11 @@ extension Repository { } } - func dictionary( + func dict( query: String, with result: (SQLite.Statement.Element) -> Result ) -> [Int64: Result] { - print("DEBUG: Repository.dictionary: \(query)") + print("DEBUG: Repository.dict: \(query)") do { var dict: [Int64: Result] = [:] for row in try db.prepare(query) { diff --git a/MMEX/Repositories/RepositoryProtocol.swift b/MMEX/Repositories/RepositoryProtocol.swift index 64c149b0..96cfb79d 100644 --- a/MMEX/Repositories/RepositoryProtocol.swift +++ b/MMEX/Repositories/RepositoryProtocol.swift @@ -70,7 +70,7 @@ extension RepositoryProtocol { } } - func dictionary( + func dict( from table: SQLite.Table, with result: (SQLite.Row) -> Result = Self.fetchData ) -> [Int64: Result] { diff --git a/MMEX/ViewModels/InsightsViewModel.swift b/MMEX/ViewModels/InsightsViewModel.swift index 2a8e3ab5..b5870629 100644 --- a/MMEX/ViewModels/InsightsViewModel.swift +++ b/MMEX/ViewModels/InsightsViewModel.swift @@ -7,29 +7,32 @@ import Foundation import SwiftUI +import SQLite import Combine class InsightsViewModel: ObservableObject { private var dataManager: DataManager - - + + @Published var stats: [TransactionData] = [] // all transactions // Published properties for the view to observe @Published var recentStats: [TransactionData] = [] @Published var startDate: Date @Published var endDate: Date - + @Published var accountInfo = InsightsAccountInfo() + private var cancellables = Set() - + init(dataManager: DataManager) { self.dataManager = dataManager self.startDate = Calendar.current.date(byAdding: .month, value: -1, to: Date()) ?? Date() self.endDate = Date() - + // Load transactions on initialization + loadAccountInfo() loadRecentTransactions() loadTransactions() - + // Automatically reload transactions when date range changes $startDate .combineLatest($endDate) @@ -38,31 +41,68 @@ class InsightsViewModel: ObservableObject { } .store(in: &cancellables) } - + func loadRecentTransactions() { let repository = dataManager.transactionRepository - // Fetch transactions asynchronously DispatchQueue.global(qos: .background).async { let transactions = repository?.loadRecent(startDate: self.startDate, endDate: self.endDate) ?? [] - // Update the published stats on the main thread DispatchQueue.main.async { self.recentStats = transactions } } } - + func loadTransactions() { - if let repository = dataManager.transactionRepository { - // Fetch transactions asynchronously - DispatchQueue.global(qos: .background).async { - let transactions = repository.load() + 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 + } + } + } + + func loadAccountInfo() { + self.accountInfo.today = String(self.endDate.ISO8601Format().dropLast()) + let repository = dataManager.accountRepository + typealias A = AccountRepository + let table = A.table + .filter(A.table[A.col_status] == AccountStatus.open.rawValue) + + // fetch open accounts + DispatchQueue.global(qos: .background).async { + let dataByType = repository?.selectByType( + from: table.order(AccountRepository.col_name) + ) ?? [:] + // Update the published stats on the main thread + DispatchQueue.main.async { + self.accountInfo.dataByType = dataByType + } + } - // Update the published stats on the main thread - DispatchQueue.main.async { - self.stats = transactions - } + // fetch flow of open accounts until today + DispatchQueue.global(qos: .background).async { + let flowByStatus = repository?.dictFlowByStatus( + from: table, + maxDate: self.accountInfo.today + ) ?? [:] + DispatchQueue.main.async { + self.accountInfo.flowUntilToday = flowByStatus + } + } + + // fetch flow of open accounts after today + DispatchQueue.global(qos: .background).async { + let flowByStatus = repository?.dictFlowByStatus( + from: table, + minDate: self.accountInfo.today + "z" + ) ?? [:] + DispatchQueue.main.async { + self.accountInfo.flowUntilToday = flowByStatus } } } diff --git a/MMEX/Views/Insights/IncomeAndExpense.swift b/MMEX/Views/Insights/IncomeExpenseView.swift similarity index 93% rename from MMEX/Views/Insights/IncomeAndExpense.swift rename to MMEX/Views/Insights/IncomeExpenseView.swift index 0083ebce..1974d65b 100644 --- a/MMEX/Views/Insights/IncomeAndExpense.swift +++ b/MMEX/Views/Insights/IncomeExpenseView.swift @@ -8,8 +8,9 @@ import SwiftUI import Charts -struct IncomeAndExpense: View { +struct IncomeExpenseView: View { @Binding var stats: [TransactionData] + var body: some View { Chart() { ForEach(stats) { stat in @@ -49,5 +50,5 @@ struct IncomeAndExpense: View { } #Preview { - IncomeAndExpense(stats: .constant(TransactionData.sampleData)) + IncomeExpenseView(stats: .constant(TransactionData.sampleData)) } diff --git a/MMEX/Views/Insights/InsightsAccountView.swift b/MMEX/Views/Insights/InsightsAccountView.swift new file mode 100644 index 00000000..1b5d030e --- /dev/null +++ b/MMEX/Views/Insights/InsightsAccountView.swift @@ -0,0 +1,88 @@ +// +// Summary.swift +// MMEX +// +// Created by Lisheng Guan on 2024/9/29. +// + +import SwiftUI + +struct InsightsAccountInfo { + var dataByType: [AccountType: [AccountData]] = [:] + var today: String = "" + var flowUntilToday: [Int64: AccountFlowByStatus] = [:] + var flowAfterToday: [Int64: AccountFlowByStatus] = [:] +} + +struct InsightsAccountView: View { + @EnvironmentObject var dataManager: DataManager + @Binding var accountInfo: InsightsAccountInfo + @State private var expandedSections: [AccountType: Bool] = [:] + + static let typeOrder: [AccountType] = [ .checking, .creditCard, .cash, .loan, .term, .asset, .shares ] + + var body: some View { + VStack { + ForEach(Self.typeOrder) { accountType in + if let accounts = accountInfo.dataByType[accountType] { + Section( + header: HStack { + Button(action: { + // Toggle expanded/collapsed state + expandedSections[accountType]?.toggle() + }) { + HStack { + Image(systemName: accountType.symbolName) + .frame(width: 5, alignment: .leading) // Adjust width as needed + .font(.system(size: 16, weight: .bold)) // Customize size and weight + .foregroundColor(.blue) // Customize icon style + Text(accountType.rawValue) + .font(.subheadline) + .padding(.leading) + + Spacer(minLength: 10) + + // Expand or collapse indicator + Image(systemName: expandedSections[accountType] == true ? "chevron.down" : "chevron.right") + .foregroundColor(.gray) + } + } + } + ) { + // Show account list based on expandedSections state + if expandedSections[accountType] == true { + ForEach(accounts) { account in + HStack { + Text(account.name) + .font(.subheadline) + + Spacer(minLength: 10) + + let flow = accountInfo.flowUntilToday[account.id] + let total = (flow?.diffTotal ?? 0.0) + account.initialBal + if let currency = dataManager.currencyFormat[account.currencyId] { + Text(currency.format(amount: total)) + .font(.subheadline) + } else { + Text(String(format: "%.2f", total)) + .font(.subheadline) + } + } + .padding(.horizontal) + } + } + } + } + } + } + .onAppear { + for accountType in Self.typeOrder { + expandedSections[accountType] = true + } + } + } +} + +#Preview { + //InsightsAccountView(stats: .constant(TransactionData.sampleData)) +} diff --git a/MMEX/Views/Insights/Summary.swift b/MMEX/Views/Insights/InsightsSummary.swift similarity index 85% rename from MMEX/Views/Insights/Summary.swift rename to MMEX/Views/Insights/InsightsSummary.swift index 9f0c2299..0e8ae565 100644 --- a/MMEX/Views/Insights/Summary.swift +++ b/MMEX/Views/Insights/InsightsSummary.swift @@ -8,7 +8,7 @@ import SwiftUI import Charts -struct Summary: View { +struct InsightsSummaryView: View { @Binding var stats: [TransactionData] var body: some View { @@ -27,5 +27,5 @@ struct Summary: View { } #Preview { - Summary(stats: .constant(TransactionData.sampleData)) + InsightsSummaryView(stats: .constant(TransactionData.sampleData)) } diff --git a/MMEX/Views/Insights/InsightsView.swift b/MMEX/Views/Insights/InsightsView.swift index 53eef46f..b98e7f70 100644 --- a/MMEX/Views/Insights/InsightsView.swift +++ b/MMEX/Views/Insights/InsightsView.swift @@ -16,7 +16,15 @@ struct InsightsView: View { ScrollView { VStack(spacing: 20) { Section { - Summary(stats: $viewModel.stats) + InsightsAccountView(accountInfo: $viewModel.accountInfo) + } header: { + Text("Account Balance") + .font(.headline) + .padding(.horizontal) + } + + Section { + InsightsSummaryView(stats: $viewModel.stats) } header: { Text("Account Income Summary") .font(.headline) @@ -43,7 +51,7 @@ struct InsightsView: View { } Section { - IncomeAndExpense(stats: $viewModel.recentStats) + IncomeExpenseView(stats: $viewModel.recentStats) } header: { Text("Income vs Expense Over Time") .font(.headline) From ba911ee268e5f085eb3e0abf49b2c1d23283c031 Mon Sep 17 00:00:00 2001 From: George Ef Date: Mon, 30 Sep 2024 01:04:04 +0200 Subject: [PATCH 2/5] add status choice --- MMEX/Views/Insights/InsightsAccountView.swift | 38 ++++++++++++++++--- MMEX/Views/Insights/InsightsView.swift | 5 ++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/MMEX/Views/Insights/InsightsAccountView.swift b/MMEX/Views/Insights/InsightsAccountView.swift index 1b5d030e..c5b2df03 100644 --- a/MMEX/Views/Insights/InsightsAccountView.swift +++ b/MMEX/Views/Insights/InsightsAccountView.swift @@ -17,12 +17,32 @@ struct InsightsAccountInfo { struct InsightsAccountView: View { @EnvironmentObject var dataManager: DataManager @Binding var accountInfo: InsightsAccountInfo + @Binding var statusChoice: Int @State private var expandedSections: [AccountType: Bool] = [:] - + static let typeOrder: [AccountType] = [ .checking, .creditCard, .cash, .loan, .term, .asset, .shares ] + static let statusChoices = [ + ("Account Balance", "Reconciled Balance"), + ("Account Balance", "Total Balance"), + ("Account Flow", "None"), + ("Account Flow", "Duplicate"), + ("Account Flow", "Follow up"), + ("Account Flow", "Void") + ] var body: some View { VStack { + HStack { + Spacer() + Picker("Status Choice", selection: $statusChoice) { + ForEach(0.. Date: Mon, 30 Sep 2024 01:18:01 +0200 Subject: [PATCH 3/5] do not show flow if nil --- MMEX/Views/Insights/InsightsAccountView.swift | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/MMEX/Views/Insights/InsightsAccountView.swift b/MMEX/Views/Insights/InsightsAccountView.swift index c5b2df03..73823dfc 100644 --- a/MMEX/Views/Insights/InsightsAccountView.swift +++ b/MMEX/Views/Insights/InsightsAccountView.swift @@ -79,21 +79,23 @@ struct InsightsAccountView: View { Spacer(minLength: 10) let flowByStatus = accountInfo.flowUntilToday[account.id] - let value: Double = switch Self.statusChoices[statusChoice].1 { + let value: Double? = switch Self.statusChoices[statusChoice].1 { case "Reconciled Balance" : (flowByStatus?.diffReconciled ?? 0.0) + account.initialBal case "Total Balance" : (flowByStatus?.diffTotal ?? 0.0) + account.initialBal - case "None" : (flowByStatus?[.none]?.diff ?? 0.0) - case "Duplicate" : (flowByStatus?[.duplicate]?.diff ?? 0.0) - case "Follow up" : (flowByStatus?[.followUp]?.diff ?? 0.0) - case "Void" : (flowByStatus?[.void]?.diff ?? 0.0) - default : 0.0 + case "None" : flowByStatus?[.none]?.diff + case "Duplicate" : flowByStatus?[.duplicate]?.diff + case "Follow up" : flowByStatus?[.followUp]?.diff + case "Void" : flowByStatus?[.void]?.diff + default : nil } - if let currency = dataManager.currencyFormat[account.currencyId] { - Text(currency.format(amount: value)) - .font(.subheadline) - } else { - Text(String(format: "%.2f", value)) - .font(.subheadline) + if let value { + if let currency = dataManager.currencyFormat[account.currencyId] { + Text(currency.format(amount: value)) + .font(.subheadline) + } else { + Text(String(format: "%.2f", value)) + .font(.subheadline) + } } } .padding(.horizontal) From 77c6a79100b23e6247ee48028c6e7ba8c0149f30 Mon Sep 17 00:00:00 2001 From: George Ef Date: Mon, 30 Sep 2024 01:30:19 +0200 Subject: [PATCH 4/5] fix spacing --- MMEX/Views/Insights/InsightsAccountView.swift | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/MMEX/Views/Insights/InsightsAccountView.swift b/MMEX/Views/Insights/InsightsAccountView.swift index 73823dfc..7a5dde28 100644 --- a/MMEX/Views/Insights/InsightsAccountView.swift +++ b/MMEX/Views/Insights/InsightsAccountView.swift @@ -45,6 +45,7 @@ struct InsightsAccountView: View { ForEach(Self.typeOrder) { accountType in if let accounts = accountInfo.dataByType[accountType] { + Spacer(minLength: 8) Section( header: HStack { Button(action: { @@ -71,34 +72,37 @@ struct InsightsAccountView: View { ) { // Show account list based on expandedSections state if expandedSections[accountType] == true { - ForEach(accounts) { account in - HStack { - Text(account.name) - .font(.subheadline) - - Spacer(minLength: 10) - - let flowByStatus = accountInfo.flowUntilToday[account.id] - let value: Double? = switch Self.statusChoices[statusChoice].1 { - case "Reconciled Balance" : (flowByStatus?.diffReconciled ?? 0.0) + account.initialBal - case "Total Balance" : (flowByStatus?.diffTotal ?? 0.0) + account.initialBal - case "None" : flowByStatus?[.none]?.diff - case "Duplicate" : flowByStatus?[.duplicate]?.diff - case "Follow up" : flowByStatus?[.followUp]?.diff - case "Void" : flowByStatus?[.void]?.diff - default : nil - } - if let value { - if let currency = dataManager.currencyFormat[account.currencyId] { - Text(currency.format(amount: value)) - .font(.subheadline) - } else { - Text(String(format: "%.2f", value)) - .font(.subheadline) + VStack(spacing: 8) { + Spacer(minLength: 2) + ForEach(accounts) { account in + HStack { + Text(account.name) + .font(.subheadline) + + Spacer(minLength: 10) + + let flowByStatus = accountInfo.flowUntilToday[account.id] + let value: Double? = switch Self.statusChoices[statusChoice].1 { + case "Reconciled Balance" : (flowByStatus?.diffReconciled ?? 0.0) + account.initialBal + case "Total Balance" : (flowByStatus?.diffTotal ?? 0.0) + account.initialBal + case "None" : flowByStatus?[.none]?.diff + case "Duplicate" : flowByStatus?[.duplicate]?.diff + case "Follow up" : flowByStatus?[.followUp]?.diff + case "Void" : flowByStatus?[.void]?.diff + default : nil + } + if let value { + if let currency = dataManager.currencyFormat[account.currencyId] { + Text(currency.format(amount: value)) + .font(.subheadline) + } else { + Text(String(format: "%.2f", value)) + .font(.subheadline) + } } } + //.padding(.horizontal) } - .padding(.horizontal) } } } From 8ddf134d470bada834e3010d68c350ce3807907f Mon Sep 17 00:00:00 2001 From: George Ef Date: Mon, 30 Sep 2024 02:14:15 +0200 Subject: [PATCH 5/5] add background color --- MMEX/Views/Insights/InsightsAccountView.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/MMEX/Views/Insights/InsightsAccountView.swift b/MMEX/Views/Insights/InsightsAccountView.swift index 7a5dde28..1c0cb4c8 100644 --- a/MMEX/Views/Insights/InsightsAccountView.swift +++ b/MMEX/Views/Insights/InsightsAccountView.swift @@ -108,7 +108,11 @@ struct InsightsAccountView: View { } } } + .padding(.horizontal, 8) } + .padding(.bottom, 8) + .background(Color(.systemGray5)) + .cornerRadius(8) .onAppear { for accountType in Self.typeOrder { expandedSections[accountType] = true @@ -118,5 +122,8 @@ struct InsightsAccountView: View { } #Preview { - //InsightsAccountView(stats: .constant(TransactionData.sampleData)) + InsightsAccountView( + accountInfo: .constant(InsightsAccountInfo()), + statusChoice: .constant(1) + ) }