diff --git a/MMEX.xcodeproj/project.pbxproj b/MMEX.xcodeproj/project.pbxproj index 09e7c832..ad7e2a6b 100644 --- a/MMEX.xcodeproj/project.pbxproj +++ b/MMEX.xcodeproj/project.pbxproj @@ -26,6 +26,10 @@ 920824162CA4C95F00388AB2 /* ReportRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920824152CA4C95F00388AB2 /* ReportRepository.swift */; }; 920824182CA4CF4300388AB2 /* AttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920824172CA4CF4300388AB2 /* AttachmentData.swift */; }; 9208241A2CA4D35700388AB2 /* AttachmentRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920824192CA4D35700388AB2 /* AttachmentRepository.swift */; }; + 9208241C2CA4DFFC00388AB2 /* TransactionLinkData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9208241B2CA4DFFC00388AB2 /* TransactionLinkData.swift */; }; + 9208241E2CA4E1C500388AB2 /* TransactionLinkRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9208241D2CA4E1C500388AB2 /* TransactionLinkRepository.swift */; }; + 920824202CA4E5C200388AB2 /* TagLinkData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9208241F2CA4E5C200388AB2 /* TagLinkData.swift */; }; + 920824222CA4E67200388AB2 /* TagLinkRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920824212CA4E67200388AB2 /* TagLinkRepository.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 */; }; @@ -118,6 +122,10 @@ 920824152CA4C95F00388AB2 /* ReportRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportRepository.swift; sourceTree = ""; }; 920824172CA4CF4300388AB2 /* AttachmentData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentData.swift; sourceTree = ""; }; 920824192CA4D35700388AB2 /* AttachmentRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentRepository.swift; sourceTree = ""; }; + 9208241B2CA4DFFC00388AB2 /* TransactionLinkData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionLinkData.swift; sourceTree = ""; }; + 9208241D2CA4E1C500388AB2 /* TransactionLinkRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionLinkRepository.swift; sourceTree = ""; }; + 9208241F2CA4E5C200388AB2 /* TagLinkData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagLinkData.swift; sourceTree = ""; }; + 920824212CA4E67200388AB2 /* TagLinkRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagLinkRepository.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 = ""; }; @@ -332,11 +340,13 @@ 920823FB2CA4A15C00388AB2 /* StockHistoryRepository.swift */, A3C142A12C90267F00D3CEC0 /* CategoryRepository.swift */, A3C142932C8FE13E00D3CEC0 /* PayeeRepository.swift */, - 920823FF2CA4A4AA00388AB2 /* TagRepository.swift */, A3C142952C8FE15E00D3CEC0 /* TransactionRepository.swift */, 920824052CA4AE0A00388AB2 /* TransactionSplitRepository.swift */, + 9208241D2CA4E1C500388AB2 /* TransactionLinkRepository.swift */, 929EF66E2CA0BC2D0051A3E6 /* ScheduledRepository.swift */, 920824072CA4B05700388AB2 /* ScheduledSplitRepository.swift */, + 920823FF2CA4A4AA00388AB2 /* TagRepository.swift */, + 920824212CA4E67200388AB2 /* TagLinkRepository.swift */, 920824192CA4D35700388AB2 /* AttachmentRepository.swift */, 9208240F2CA4C5CD00388AB2 /* BudgetYearRepository.swift */, 920824112CA4C6A400388AB2 /* BudgetTableRepository.swift */, @@ -403,11 +413,13 @@ 920823F92CA49A4B00388AB2 /* StockHistoryData.swift */, A3C1429F2C9025D600D3CEC0 /* CategoryData.swift */, A3C1424D2C8B335900D3CEC0 /* PayeeData.swift */, - 920823FD2CA4A3F700388AB2 /* TagData.swift */, A3C142662C8F2AF500D3CEC0 /* TransactionData.swift */, 920824012CA4AD0C00388AB2 /* TransactionSplitData.swift */, + 9208241B2CA4DFFC00388AB2 /* TransactionLinkData.swift */, 929EF66C2CA0BA5E0051A3E6 /* ScheduledData.swift */, 920824032CA4ADC100388AB2 /* ScheduledSplitData.swift */, + 920823FD2CA4A3F700388AB2 /* TagData.swift */, + 9208241F2CA4E5C200388AB2 /* TagLinkData.swift */, 920824172CA4CF4300388AB2 /* AttachmentData.swift */, 920824092CA4C2AD00388AB2 /* BudgetYearData.swift */, 9208240B2CA4C31B00388AB2 /* BudgetTableData.swift */, @@ -559,6 +571,7 @@ A39B1B362C99A0C2003E5562 /* CurrencyEditView.swift in Sources */, A3363EE72C93249D004696C7 /* CategoryAddView.swift in Sources */, A3C142522C8B37E700D3CEC0 /* PayeeDetailView.swift in Sources */, + 920824222CA4E67200388AB2 /* TagLinkRepository.swift in Sources */, A37E7D962C9B219400B4ECFC /* InfotableRepository.swift in Sources */, A3C142442C89C8FA00D3CEC0 /* AccountData.swift in Sources */, A3C142AE2C9134DD00D3CEC0 /* InsightsView.swift in Sources */, @@ -566,6 +579,7 @@ 9208240A2CA4C2AD00388AB2 /* BudgetYearData.swift in Sources */, A3363EEB2C93BF62004696C7 /* CurrencyRepository.swift in Sources */, A37E7D942C9B217500B4ECFC /* InfotableData.swift in Sources */, + 9208241E2CA4E1C500388AB2 /* TransactionLinkRepository.swift in Sources */, 9208241A2CA4D35700388AB2 /* AttachmentRepository.swift in Sources */, A37E7D882C9AC2E600B4ECFC /* VersionInfoView.swift in Sources */, 920823F62CA498A200388AB2 /* CurrencyHistoryData.swift in Sources */, @@ -580,10 +594,12 @@ 920824022CA4AD0C00388AB2 /* TransactionSplitData.swift in Sources */, A3363EE32C9323A5004696C7 /* CategoryEditView.swift in Sources */, 920824102CA4C5CD00388AB2 /* BudgetYearRepository.swift in Sources */, + 920824202CA4E5C200388AB2 /* TagLinkData.swift in Sources */, A3C142652C8ED8EA00D3CEC0 /* AccountEditView.swift in Sources */, A3C142632C8ED8C000D3CEC0 /* AccountAddView.swift in Sources */, 929EF6712CA3676B0051A3E6 /* MMEXDocument.swift in Sources */, 929EF6692CA034770051A3E6 /* Repository.swift in Sources */, + 9208241C2CA4DFFC00388AB2 /* TransactionLinkData.swift in Sources */, A3C142A22C90267F00D3CEC0 /* CategoryRepository.swift in Sources */, A37E7D862C9AC2D000B4ECFC /* AboutView.swift in Sources */, 9208240C2CA4C31B00388AB2 /* BudgetTableData.swift in Sources */, diff --git a/MMEX/DatabaseManager.swift b/MMEX/DatabaseManager.swift index 1c815ba6..21558b5c 100644 --- a/MMEX/DatabaseManager.swift +++ b/MMEX/DatabaseManager.swift @@ -59,44 +59,26 @@ class DataManager { } return nil } - - func getRepository() -> Repository { - return Repository(db: db) - } - - func getInfotableRepository() -> InfotableRepository { - return InfotableRepository(db: db) - } - - func getCurrencyRepository() -> CurrencyRepository { - return CurrencyRepository(db: db) - } - - func getAccountRepository() -> AccountRepository { - return AccountRepository(db: db) - } - - func getAssetRepository() -> AssetRepository { - return AssetRepository(db: db) - } - func getStockRepository() -> StockRepository { - return StockRepository(db: db) - } - - func getCategoryRepository() -> CategoryRepository { - return CategoryRepository(db: db) - } - - func getPayeeRepository() -> PayeeRepository { - return PayeeRepository(db: db) - } - - func getTransactionRepository() -> TransactionRepository { - return TransactionRepository(db: db) - } - - func getScheduledRepository() -> ScheduledRepository { - return ScheduledRepository(db: db) - } + func getRepository() -> Repository { Repository(db: db) } + func getInfotableRepository() -> InfotableRepository { InfotableRepository(db: db) } + func getCurrencyRepository() -> CurrencyRepository { CurrencyRepository(db: db) } + func getCurrencyHistoryRepository() -> CurrencyRepository { CurrencyRepository(db: db) } + func getAccountRepository() -> AccountRepository { AccountRepository(db: db) } + func getAssetRepository() -> AssetRepository { AssetRepository(db: db) } + func getStockRepository() -> StockRepository { StockRepository(db: db) } + func getStockHistoryRepository() -> StockRepository { StockRepository(db: db) } + func getCategoryRepository() -> CategoryRepository { CategoryRepository(db: db) } + func getPayeeRepository() -> PayeeRepository { PayeeRepository(db: db) } + func getTransactionRepository() -> TransactionRepository { TransactionRepository(db: db) } + func getTransactionSplitRepository() -> TransactionSplitRepository { TransactionSplitRepository(db: db) } + func getTransactionLinkRepository() -> TransactionLinkRepository { TransactionLinkRepository(db: db) } + func getScheduledRepository() -> ScheduledRepository { ScheduledRepository(db: db) } + func getScheduledSplitRepository() -> ScheduledSplitRepository { ScheduledSplitRepository(db: db) } + func getTagRepository() -> TagRepository { TagRepository(db: db) } + func getTagLinkRepository() -> TagLinkRepository { TagLinkRepository(db: db) } + func getAttachmentRepository() -> AttachmentRepository { AttachmentRepository(db: db) } + func getBudgetYearRepository() -> BudgetYearRepository { BudgetYearRepository(db: db) } + func getBudgetTableRepository() -> BudgetTableRepository { BudgetTableRepository(db: db) } + func getReportRepository() -> ReportRepository { ReportRepository(db: db) } } diff --git a/MMEX/Models/TagLinkData.swift b/MMEX/Models/TagLinkData.swift new file mode 100644 index 00000000..309c083d --- /dev/null +++ b/MMEX/Models/TagLinkData.swift @@ -0,0 +1,33 @@ +// +// TagLinkData.swift +// MMEX +// +// Created 2024-09-26 by George Ef (george.a.ef@gmail.com) +// + +import Foundation +import SQLite + +struct TagLinkData: ExportableEntity { + var id : Int64 = 0 + var tagId : Int64 = 0 + var refType : RefType = RefType.transaction + var refId : Int64 = 0 + static let refTypes: Set = [ + .transaction, .transactionSplit, + .scheduled, .scheduledSplit, + ] +} + +extension TagLinkData: DataProtocol { + static let dataName = "TagLinkData" + + func shortDesc() -> String { + "\(self.id)" + } +} + +extension TagLinkData { + static let sampleData: [TagLinkData] = [ + ] +} diff --git a/MMEX/Models/TransactionLinkData.swift b/MMEX/Models/TransactionLinkData.swift new file mode 100644 index 00000000..45b63339 --- /dev/null +++ b/MMEX/Models/TransactionLinkData.swift @@ -0,0 +1,30 @@ +// +// TransactionLinkData.swift +// MMEX +// +// Created 2024-09-26 by George Ef (george.a.ef@gmail.com) +// + +import Foundation +import SQLite + +struct TransactionLinkData: ExportableEntity { + var id : Int64 = 0 + var transId : Int64 = 0 + var refType : RefType = RefType.asset + var refId : Int64 = 0 + static let refTypes: Set = [ .asset, .stock ] +} + +extension TransactionLinkData: DataProtocol { + static let dataName = "TransactionLink" + + func shortDesc() -> String { + "\(self.id)" + } +} + +extension TransactionLinkData { + static let sampleData: [TransactionLinkData] = [ + ] +} diff --git a/MMEX/Repositories/Repository.swift b/MMEX/Repositories/Repository.swift index 47770305..42538b77 100644 --- a/MMEX/Repositories/Repository.swift +++ b/MMEX/Repositories/Repository.swift @@ -16,6 +16,24 @@ class Repository { } extension Repository { + func select( + from table: SQLite.Table, + with result: (SQLite.Row) -> Result + ) -> [Result] { + guard let db else { return [] } + do { + var data: [Result] = [] + for row in try db.prepare(table) { + data.append(result(row)) + } + print("Successfull select: \(data.count)") + return data + } catch { + print("Failed select: \(error)") + return [] + } + } + func execute(sql: String) { guard let db else { return } print("Executing sql: \(sql)") @@ -25,7 +43,7 @@ extension Repository { print("Failed to execute sql: \(error)") } } - + func execute(url: URL) { if db == nil { return } guard let contents = try? String(contentsOf: url) else { @@ -84,6 +102,25 @@ extension RepositoryProtocol { } } + func pluck(id: Int64) -> RepositoryData? { + guard let db else { return nil } + do { + if let row = try db.pluck(Self.selectQuery(from: Self.table) + .filter(Self.col_id == id) + ) { + let data = Self.selectData(row) + print("Successfull pluck for id \(id) in \(Self.repositoryName): \(data.shortDesc())") + return data + } else { + print("Unsuccefull pluck for id \(id) in \(Self.repositoryName)") + return nil + } + } catch { + print("Failed pluck for id \(id) in \(Self.repositoryName): \(error)") + return nil + } + } + func select(from table: SQLite.Table) -> [RepositoryData] { guard let db else { return [] } do { @@ -272,17 +309,6 @@ extension Repository { } } - var tagMap: [Int64: Int64] = [:] - do { - let repo = TagRepository(db: db) - repo.deleteAll() - for var data in TagData.sampleData { - let id = data.id - repo.insert(&data) - tagMap[id] = data.id - } - } - var transactionMap: [Int64: Int64] = [:] do { let repo = TransactionRepository(db: db) @@ -311,6 +337,22 @@ extension Repository { } } + var transactionLinkMap: [Int64: Int64] = [:] + do { + let repo = TransactionLinkRepository(db: db) + repo.deleteAll() + for var data in TransactionLinkData.sampleData { + let id = data.id + data.refId = switch data.refType { + case .stock : stockMap[data.refId] ?? data.refId + case .asset : assetMap[data.refId] ?? data.refId + default: data.refId + } + repo.insert(&data) + transactionLinkMap[id] = data.id + } + } + var scheduledMap: [Int64: Int64] = [:] do { let repo = ScheduledRepository(db: db) @@ -339,14 +381,32 @@ extension Repository { } } - var budgetYearMap: [Int64: Int64] = [:] + var tagMap: [Int64: Int64] = [:] do { - let repo = BudgetYearRepository(db: db) + let repo = TagRepository(db: db) repo.deleteAll() - for var data in BudgetYearData.sampleData { + for var data in TagData.sampleData { let id = data.id repo.insert(&data) - budgetYearMap[id] = data.id + tagMap[id] = data.id + } + } + + var tagLinkMap: [Int64: Int64] = [:] + do { + let repo = TagLinkRepository(db: db) + repo.deleteAll() + for var data in TagLinkData.sampleData { + let id = data.id + data.refId = switch data.refType { + case .transaction : transactionMap[data.refId] ?? data.refId + case .transactionSplit : transactionSplitMap[data.refId] ?? data.refId + case .scheduled : scheduledMap[data.refId] ?? data.refId + case .scheduledSplit : scheduledSplitMap[data.refId] ?? data.refId + default: data.refId + } + repo.insert(&data) + tagLinkMap[id] = data.id } } @@ -371,6 +431,17 @@ extension Repository { } } + var budgetYearMap: [Int64: Int64] = [:] + do { + let repo = BudgetYearRepository(db: db) + repo.deleteAll() + for var data in BudgetYearData.sampleData { + let id = data.id + repo.insert(&data) + budgetYearMap[id] = data.id + } + } + var budgetTableMap: [Int64: Int64] = [:] do { let repo = BudgetTableRepository(db: db) diff --git a/MMEX/Repositories/TagLinkRepository.swift b/MMEX/Repositories/TagLinkRepository.swift new file mode 100644 index 00000000..6997729a --- /dev/null +++ b/MMEX/Repositories/TagLinkRepository.swift @@ -0,0 +1,70 @@ +// +// TagLinkRepository.swift +// MMEX +// +// Created 2024-09-26 by George Ef (george.a.ef@gmail.com) +// + +import Foundation +import SQLite + +class TagLinkRepository: RepositoryProtocol { + typealias RepositoryData = TagLinkData + + let db: Connection? + init(db: Connection?) { + self.db = db + } + + static let repositoryName = "TAGLINK_V1" + static let table = SQLite.Table(repositoryName) + + // column | type | other + // --------------------+---------+------ + // TAGLINKID | INTEGER | PRIMARY KEY + // REFTYPE | TEXT | NOT NULL (Transaction, TransactionSplit, ...) + // REFID | INTEGER | NOT NULL + // TAGID | INTEGER | NOT NULL + // | | UNIQUE(REFTYPE, REFID, TAGID) + + // column expressions + static let col_id = SQLite.Expression("TAGLINKID") + static let col_tagId = SQLite.Expression("TAGID") + static let col_refType = SQLite.Expression("REFTYPE") + static let col_refId = SQLite.Expression("REFID") + + static func selectQuery(from table: SQLite.Table) -> SQLite.Table { + return table.select( + col_id, + col_tagId, + col_refType, + col_refId + ) + } + + static func selectData(_ row: SQLite.Row) -> TagLinkData { + return TagLinkData( + id : row[col_id], + tagId : row[col_tagId], + refType : RefType(collateNoCase: row[col_refType]), + refId : row[col_refId] + ) + } + + static func itemSetters(_ link: TagLinkData) -> [SQLite.Setter] { + return [ + col_tagId <- link.tagId, + col_refType <- link.refType.rawValue, + col_refId <- link.refId + ] + } +} + +extension TagLinkRepository { + // load all tag links + func load() -> [TagLinkData] { + return select(from: Self.table + .order(Self.col_id) + ) + } +} diff --git a/MMEX/Repositories/TagRepository.swift b/MMEX/Repositories/TagRepository.swift index f7bd2b83..e79b4e10 100644 --- a/MMEX/Repositories/TagRepository.swift +++ b/MMEX/Repositories/TagRepository.swift @@ -61,4 +61,34 @@ extension TagRepository { .order(Self.col_name) ) } + + // load tags of a specific item + func load(for trans: TransactionData) -> [String] { + typealias G = TagRepository + typealias L = TagLinkRepository + typealias T = TransactionRepository + return Repository(db: db).select(from: G.table + .join(L.table, on: L.table[L.col_tagId] == G.table[G.col_id]) + .join(T.table, on: T.table[T.col_id] == L.table[L.col_refId]) + .filter(L.table[L.col_refType] == RefType.transaction.rawValue) + .filter(T.table[T.col_id] == trans.id) + .order(L.table[L.col_id]) + ) { row in + row[G.table[G.col_name]] + } + } + func load(for sched: ScheduledData) -> [String] { + typealias G = TagRepository + typealias L = TagLinkRepository + typealias T = ScheduledRepository + return Repository(db: db).select(from: G.table + .join(L.table, on: L.table[L.col_tagId] == G.table[G.col_id]) + .join(T.table, on: T.table[T.col_id] == L.table[L.col_refId]) + .filter(L.table[L.col_refType] == RefType.scheduled.rawValue) + .filter(T.table[T.col_id] == sched.id) + .order(L.table[L.col_id]) + ) { row in + row[G.table[G.col_name]] + } + } } diff --git a/MMEX/Repositories/TransactionLinkRepository.swift b/MMEX/Repositories/TransactionLinkRepository.swift new file mode 100644 index 00000000..b5b5465e --- /dev/null +++ b/MMEX/Repositories/TransactionLinkRepository.swift @@ -0,0 +1,80 @@ +// +// TransactionLinkRepository.swift +// MMEX +// +// Created 2024-09-26 by George Ef (george.a.ef@gmail.com) +// + +import Foundation +import SQLite + +class TransactionLinkRepository: RepositoryProtocol { + typealias RepositoryData = TransactionLinkData + + let db: Connection? + init(db: Connection?) { + self.db = db + } + + static let repositoryName = "TRANSLINK_V1" + static let table = SQLite.Table(repositoryName) + + // column | type | other + // ------------------+---------+------ + // TRANSLINKID | INTEGER | PRIMARY KEY + // CHECKINGACCOUNTID | INTEGER | NOT NULL + // LINKTYPE | TEXT | NOT NULL (Asset, Stock) + // LINKRECORDID | INTEGER | NOT NULL + + // column expressions + static let col_id = SQLite.Expression("TRANSLINKID") + static let col_transId = SQLite.Expression("CHECKINGACCOUNTID") + static let col_refType = SQLite.Expression("LINKTYPE") + static let col_refId = SQLite.Expression("LINKRECORDID") + + static func selectQuery(from table: SQLite.Table) -> SQLite.Table { + return table.select( + col_id, + col_transId, + col_refType, + col_refId + ) + } + + static func selectData(_ row: SQLite.Row) -> TransactionLinkData { + return TransactionLinkData( + id : row[col_id], + transId : row[col_transId], + refType : RefType(collateNoCase: row[col_refType]), + refId : row[col_refId] + ) + } + + static func itemSetters(_ link: TransactionLinkData) -> [SQLite.Setter] { + return [ + col_transId <- link.transId, + col_refType <- link.refType.rawValue, + col_refId <- link.refId + ] + } +} + +extension TransactionLinkRepository { + // load all transaction links + func load() -> [TransactionLinkData] { + return select(from: Self.table + .order(Self.col_id) + ) + } + + // load links of a transaction + func load(forTransactionId transId: Int64) -> [TransactionLinkData] { + return select(from: Self.table + .filter(Self.col_transId == transId) + .order(Self.col_id) + ) + } + func load(for trans: TransactionData) -> [TransactionLinkData] { + return load(forTransactionId: trans.id) + } +} diff --git a/MMEX/ViewModels/InfotableViewModel.swift b/MMEX/ViewModels/InfotableViewModel.swift index 190afa4a..1138089e 100644 --- a/MMEX/ViewModels/InfotableViewModel.swift +++ b/MMEX/ViewModels/InfotableViewModel.swift @@ -130,7 +130,7 @@ class InfotableViewModel: ObservableObject { let loadTransactions = self.transactionRepo.loadRecent(accountId: self.defaultAccountId) DispatchQueue.main.async { - self.txns = loadTransactions + self.txns = loadTransactions.filter { txn in txn.deletedTime.isEmpty } self.txns_per_day = Dictionary(grouping: self.txns) { txn in // Extract the date portion (ignoring the time) from ISO-8601 string let formatter = DateFormatter()