diff --git a/MMEX.xcodeproj/project.pbxproj b/MMEX.xcodeproj/project.pbxproj index 8269cd7..0404f8f 100644 --- a/MMEX.xcodeproj/project.pbxproj +++ b/MMEX.xcodeproj/project.pbxproj @@ -110,6 +110,9 @@ 925EAC222CDAB03B003CEAB3 /* AttachmentReload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925EAC212CDAB03B003CEAB3 /* AttachmentReload.swift */; }; 925EAC242CDAB100003CEAB3 /* AttachmentListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925EAC232CDAB100003CEAB3 /* AttachmentListView.swift */; }; 925EAC272CDAB134003CEAB3 /* AttachmentEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925EAC262CDAB134003CEAB3 /* AttachmentEditView.swift */; }; + 925EAC292CDABF9F003CEAB3 /* FieldList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925EAC282CDABF9F003CEAB3 /* FieldList.swift */; }; + 925EAC2B2CDAC10D003CEAB3 /* FieldGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925EAC2A2CDAC10D003CEAB3 /* FieldGroup.swift */; }; + 925EAC2D2CDAC36F003CEAB3 /* FieldSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925EAC2C2CDAC36F003CEAB3 /* FieldSearch.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 */; }; @@ -281,6 +284,9 @@ 925EAC212CDAB03B003CEAB3 /* AttachmentReload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentReload.swift; sourceTree = ""; }; 925EAC232CDAB100003CEAB3 /* AttachmentListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentListView.swift; sourceTree = ""; }; 925EAC262CDAB134003CEAB3 /* AttachmentEditView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentEditView.swift; sourceTree = ""; }; + 925EAC282CDABF9F003CEAB3 /* FieldList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FieldList.swift; sourceTree = ""; }; + 925EAC2A2CDAC10D003CEAB3 /* FieldGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldGroup.swift; sourceTree = ""; }; + 925EAC2C2CDAC36F003CEAB3 /* FieldSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FieldSearch.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 = ""; }; @@ -461,6 +467,7 @@ 925EAB9C2CCDD67C003CEAB3 /* CategoryGroup.swift */, 924353362CCD2DDA0052E4BC /* PayeeGroup.swift */, 925EAC0C2CDA89BD003CEAB3 /* TagGroup.swift */, + 925EAC2A2CDAC10D003CEAB3 /* FieldGroup.swift */, 925EAC1B2CDAA900003CEAB3 /* AttachmentGroup.swift */, ); path = Group; @@ -477,6 +484,7 @@ 925EAB9E2CCDDD77003CEAB3 /* CategorySearch.swift */, 924353402CCD2FFA0052E4BC /* PayeeSearch.swift */, 925EAC0E2CDA8B1A003CEAB3 /* TagSearch.swift */, + 925EAC2C2CDAC36F003CEAB3 /* FieldSearch.swift */, 925EAC1D2CDAAD3F003CEAB3 /* AttachmentSearch.swift */, ); path = Search; @@ -496,6 +504,7 @@ 925EABB72CD6FAC7003CEAB3 /* TransactionList.swift */, 925EABB92CD6FBF7003CEAB3 /* ScheduledList.swift */, 925EAC0A2CDA8903003CEAB3 /* TagList.swift */, + 925EAC282CDABF9F003CEAB3 /* FieldList.swift */, 925EAC192CDAA813003CEAB3 /* AttachmentList.swift */, 9243531C2CC5201C0052E4BC /* ManageList.swift */, ); @@ -904,6 +913,7 @@ 929EF6612C9FF2FD0051A3E6 /* StockData.swift in Sources */, 9243534D2CCD38830052E4BC /* AssetList.swift in Sources */, 925EAB9D2CCDD67C003CEAB3 /* CategoryGroup.swift in Sources */, + 925EAC2B2CDAC10D003CEAB3 /* FieldGroup.swift in Sources */, 925EAC272CDAB134003CEAB3 /* AttachmentEditView.swift in Sources */, 925EAC1A2CDAA813003CEAB3 /* AttachmentList.swift in Sources */, A37E7D922C9AC60000B4ECFC /* ContactSupportView.swift in Sources */, @@ -913,6 +923,7 @@ 925EABB82CD6FAC7003CEAB3 /* TransactionList.swift in Sources */, A3C142502C8B366400D3CEC0 /* PayeeListView.swift in Sources */, 925EABBA2CD6FBF7003CEAB3 /* ScheduledList.swift in Sources */, + 925EAC2D2CDAC36F003CEAB3 /* FieldSearch.swift in Sources */, 925EABB22CCFFC2E003CEAB3 /* Reload.swift in Sources */, A337428A2CA8E72C00698466 /* InsightsSummary.swift in Sources */, A3C142672C8F2AF500D3CEC0 /* TransactionData.swift in Sources */, @@ -962,6 +973,7 @@ 924353492CCD36890052E4BC /* CurrencyList.swift in Sources */, 924352FA2CB355CF0052E4BC /* SettingsThemeView.swift in Sources */, 925EABAC2CCF826F003CEAB3 /* CategoryValidation.swift in Sources */, + 925EAC292CDABF9F003CEAB3 /* FieldList.swift in Sources */, 9208240A2CA4C2AD00388AB2 /* YearData.swift in Sources */, 924353392CCD2F310052E4BC /* CurrencySearch.swift in Sources */, 925EAC152CDA8EEA003CEAB3 /* TagListView.swift in Sources */, diff --git a/MMEX/Repository/FieldRepository.swift b/MMEX/Repository/FieldRepository.swift index 77c7cf3..f24c729 100644 --- a/MMEX/Repository/FieldRepository.swift +++ b/MMEX/Repository/FieldRepository.swift @@ -61,9 +61,9 @@ struct FieldRepository: RepositoryProtocol { } static func filterUsed(_ table: SQLite.Table) -> SQLite.Table { - typealias FC = FieldValueRepository - let cond = "EXISTS (" + (FC.table.select(1).where( - FC.table[FC.col_fieldId] == Self.table[Self.col_id] + typealias FV = FieldValueRepository + let cond = "EXISTS (" + (FV.table.select(1).where( + FV.table[FV.col_fieldId] == Self.table[Self.col_id] ) ).expression.description + ")" return table.filter(SQLite.Expression(literal: cond)) } diff --git a/MMEX/ViewModel/Group/FieldGroup.swift b/MMEX/ViewModel/Group/FieldGroup.swift new file mode 100644 index 0000000..58379a2 --- /dev/null +++ b/MMEX/ViewModel/Group/FieldGroup.swift @@ -0,0 +1,87 @@ +// +// FieldGroup.swift +// MMEX +// +// 2024-11-05: Created by George Ef (george.a.ef@gmail.com) +// + +import SwiftUI + +enum FieldGroupChoice: String, GroupChoiceProtocol { + case all = "All" + case used = "Used" + case refType = "Ref. Type" + case type = "Field Type" + static let defaultValue = Self.all + static let isSingleton: Set = [.all] +} + +struct FieldGroup: GroupProtocol { + typealias MainRepository = FieldRepository + typealias GroupChoice = FieldGroupChoice + let idleValue: ValueType = [] + + var choice: GroupChoice = .defaultValue + var state: LoadState = .init() + var value: ValueType + + init() { + self.value = idleValue + } + + static let groupUsed: [Bool] = [ + true, false + ] + + static let groupRefType: [RefType] = [ + .transaction, .scheduled + ] + + static let groupType: [FieldType] = [ + .string, .integer, .decimal, .boolean, .date, .time, + .singleChoice, .multiChoice, .unknown + ] +} + +extension ViewModel { + func loadFieldGroup(choice: FieldGroupChoice) { + guard + let listData = fieldList.data.readyValue, + let listUsed = fieldList.used.readyValue, + let listOrder = fieldList.order.readyValue + else { return } + + guard fieldGroup.state.loading() else { return } + log.trace("DEBUG: ViewModel.loadFieldGroup(\(choice.rawValue), main=\(Thread.isMainThread))") + + fieldGroup.choice = choice + + switch choice { + case .all: + fieldGroup.append("All", listOrder, true, true) + case .used: + let dict = Dictionary(grouping: listOrder) { listUsed.contains($0) } + for g in FieldGroup.groupUsed { + let name = g ? "Used" : "Other" + fieldGroup.append(name, dict[g] ?? [], true, g) + } + case .refType: + let dict = Dictionary(grouping: listOrder) { listData[$0]!.refType } + for g in FieldGroup.groupRefType { + fieldGroup.append(g.rawValue, dict[g] ?? [], dict[g] != nil, true) + } + case .type: + let dict = Dictionary(grouping: listOrder) { listData[$0]!.type } + for g in FieldGroup.groupType { + fieldGroup.append(g.rawValue, dict[g] ?? [], dict[g] != nil, true) + } + } + + fieldGroup.state.loaded() + log.info("INFO: ViewModel.loadFieldGroup(\(choice.rawValue), main=\(Thread.isMainThread))") + } + + func unloadFieldGroup() { + fieldGroup.unload() + } +} diff --git a/MMEX/ViewModel/Group/GroupProtocol.swift b/MMEX/ViewModel/Group/GroupProtocol.swift index 098c29a..0ef3bb7 100644 --- a/MMEX/ViewModel/Group/GroupProtocol.swift +++ b/MMEX/ViewModel/Group/GroupProtocol.swift @@ -58,6 +58,7 @@ extension ViewModel { else if MainRepository.self == C.self { loadCategoryGroup(choice: choice as! CategoryGroupChoice) } else if MainRepository.self == P.self { loadPayeeGroup(choice: choice as! PayeeGroupChoice) } else if MainRepository.self == G.self { loadTagGroup(choice: choice as! TagGroupChoice) } + else if MainRepository.self == F.self { loadFieldGroup(choice: choice as! FieldGroupChoice) } else if MainRepository.self == D.self { loadAttachmentGroup(choice: choice as! AttachmentGroupChoice) } } @@ -70,6 +71,7 @@ extension ViewModel { else if MainRepository.self == C.self { unloadCategoryGroup() } else if MainRepository.self == P.self { unloadPayeeGroup() } else if MainRepository.self == G.self { unloadTagGroup() } + else if MainRepository.self == F.self { unloadFieldGroup() } else if MainRepository.self == D.self { unloadAttachmentGroup() } } @@ -80,8 +82,9 @@ extension ViewModel { unloadStockGroup() unloadCategoryGroup() unloadPayeeGroup() - unloadAttachmentGroup() unloadTagGroup() + unloadFieldGroup() + unloadAttachmentGroup() } func unloadAll() { diff --git a/MMEX/ViewModel/List/FieldList.swift b/MMEX/ViewModel/List/FieldList.swift new file mode 100644 index 0000000..fc06c9d --- /dev/null +++ b/MMEX/ViewModel/List/FieldList.swift @@ -0,0 +1,44 @@ +// +// FieldList.swift +// MMEX +// +// 2024-11-05: Created by George Ef (george.a.ef@gmail.com) +// + +import SwiftUI +import SQLite + +struct FieldList: ListProtocol { + typealias MainRepository = FieldRepository + + var state : LoadState = .init() + var count : LoadMainCount = .init() + var data : LoadMainData = .init() + var used : LoadMainUsed = .init() + var order : LoadMainOrder = .init(order: [ + MainRepository.col_refType, MainRepository.col_description + ]) +} + +extension ViewModel { + func loadFieldList() async { + guard fieldList.reloading() else { return } + let ok = await withTaskGroup(of: Bool.self) { taskGroup -> Bool in + let ok = [ + load(&taskGroup, keyPath: \Self.fieldList.data), + load(&taskGroup, keyPath: \Self.fieldList.used), + load(&taskGroup, keyPath: \Self.fieldList.order) + ].allSatisfy { $0 } + return await taskGroupOk(taskGroup, ok) + } + fieldList.loaded(ok: ok) + } + + func unloadFieldList() { + guard fieldList.unloading() else { return } + fieldList.data.unload() + fieldList.used.unload() + fieldList.order.unload() + fieldList.unloaded() + } +} diff --git a/MMEX/ViewModel/List/ListProtocol.swift b/MMEX/ViewModel/List/ListProtocol.swift index 940df86..5454b1a 100644 --- a/MMEX/ViewModel/List/ListProtocol.swift +++ b/MMEX/ViewModel/List/ListProtocol.swift @@ -76,6 +76,7 @@ extension ViewModel { else if MainRepository.self == C.self { await loadCategoryList() } else if MainRepository.self == P.self { await loadPayeeList() } else if MainRepository.self == G.self { await loadTagList() } + else if MainRepository.self == F.self { await loadFieldList() } else if MainRepository.self == D.self { await loadAttachmentList() } } @@ -89,6 +90,7 @@ extension ViewModel { else if MainRepository.self == C.self { unloadCategoryList() } else if MainRepository.self == P.self { unloadPayeeList() } else if MainRepository.self == G.self { unloadTagList() } + else if MainRepository.self == F.self { unloadFieldList() } else if MainRepository.self == D.self { unloadAttachmentList() } } @@ -101,6 +103,7 @@ extension ViewModel { await loadCategoryList() await loadPayeeList() await loadTagList() + await loadFieldList() await loadAttachmentList() await loadManageList() } @@ -114,6 +117,7 @@ extension ViewModel { unloadCategoryList() unloadPayeeList() unloadTagList() + unloadFieldList() unloadAttachmentList() unloadManegeList() } diff --git a/MMEX/ViewModel/List/ManageList.swift b/MMEX/ViewModel/List/ManageList.swift index 7a56e91..ed39a2b 100644 --- a/MMEX/ViewModel/List/ManageList.swift +++ b/MMEX/ViewModel/List/ManageList.swift @@ -23,6 +23,7 @@ extension ViewModel { load(&taskGroup, keyPath: \Self.transactionList.count), load(&taskGroup, keyPath: \Self.scheduledList.count), load(&taskGroup, keyPath: \Self.tagList.count), + load(&taskGroup, keyPath: \Self.fieldList.count), load(&taskGroup, keyPath: \Self.attachmentList.count), ].allSatisfy { $0 } return await taskGroupOk(taskGroup, ok) @@ -49,6 +50,7 @@ extension ViewModel { transactionList.count.unload() scheduledList.count.unload() tagList.count.unload() + fieldList.count.unload() attachmentList.count.unload() manageList.unloaded() } diff --git a/MMEX/ViewModel/Search/FieldSearch.swift b/MMEX/ViewModel/Search/FieldSearch.swift new file mode 100644 index 0000000..eb247f3 --- /dev/null +++ b/MMEX/ViewModel/Search/FieldSearch.swift @@ -0,0 +1,45 @@ +// +// FieldSaerch.swift +// MMEX +// +// 2024-11-05: Created by George Ef (george.a.ef@gmail.com) +// + +import SwiftUI + +struct FieldSearch: SearchProtocol { + var area: [SearchArea] = [ + ("Description", true, {[ $0.description ]}, nil), + ("Properties", false, {[ $0.properties ]}, nil), + ] + var key: String = "" +} + +extension ViewModel { + func fieldGroupIsVisible(_ g: Int, search: FieldSearch + ) -> Bool? { + guard + let listData = fieldList.data.readyValue, + let groupData = fieldGroup.readyValue + else { return nil } + + if search.isEmpty { + return switch fieldGroup.choice { + case .refType, .type: !groupData[g].dataId.isEmpty + default: true + } + } + return groupData[g].dataId.first(where: { search.match(self, listData[$0]!) }) != nil + } + + func searchFieldGroup(search: FieldSearch, expand: Bool = false ) { + guard fieldGroup.state == .ready else { return } + for g in 0 ..< fieldGroup.value.count { + guard let isVisible = fieldGroupIsVisible(g, search: search) else { return } + fieldGroup.value[g].isVisible = isVisible + if (expand || !search.isEmpty) && isVisible { + fieldGroup.value[g].isExpanded = true + } + } + } +} diff --git a/MMEX/ViewModel/Search/SearchProtocol.swift b/MMEX/ViewModel/Search/SearchProtocol.swift index cd487df..74f0cfd 100644 --- a/MMEX/ViewModel/Search/SearchProtocol.swift +++ b/MMEX/ViewModel/Search/SearchProtocol.swift @@ -71,6 +71,8 @@ extension ViewModel { searchPayeeGroup(search: search as! PayeeSearch, expand: expand) } else if GroupType.MainRepository.self == G.self { searchTagGroup(search: search as! TagSearch, expand: expand) + } else if GroupType.MainRepository.self == F.self { + searchFieldGroup(search: search as! FieldSearch, expand: expand) } else if GroupType.MainRepository.self == D.self { searchAttachmentGroup(search: search as! AttachmentSearch, expand: expand) } diff --git a/MMEX/ViewModel/ViewModel.swift b/MMEX/ViewModel/ViewModel.swift index 2b8e01c..83eb004 100644 --- a/MMEX/ViewModel/ViewModel.swift +++ b/MMEX/ViewModel/ViewModel.swift @@ -63,6 +63,8 @@ class ViewModel: ObservableObject { typealias F = FieldRepository typealias FV = FieldValueRepository + @Published var fieldList : FieldList = .init() + @Published var fieldGroup : FieldGroup = .init() typealias D = AttachmentRepository @Published var attachmentList : AttachmentList = .init()