diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 997c30e5..eabce39d 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -16,7 +16,14 @@ jobs: uses: actions/checkout@v4 - name: Clone SQLite.swift from GitHub run: | - git clone https://github.com/stephencelis/SQLite.swift.git ../SQLite.swift + git clone --branch 0.15.3 --single-branch https://github.com/stephencelis/SQLite.swift.git ../SQLite.swift + - name: Reset SDK Cache + run: | + echo "Resetting SDK cache..." + sudo rm -rf ~/Library/Caches/com.apple.dt.Xcode/SDKs + sudo rm -rf ~/Library/Developer/Xcode/DerivedData + sudo xcrun --sdk iphoneos --show-sdk-path + echo "SDK cache reset completed." - name: Set Default Scheme run: | scheme_list=$(xcodebuild -list -json | tr -d "\n") diff --git a/MMEX/Data/CategoryData.swift b/MMEX/Data/CategoryData.swift index ed00726a..8a90d3be 100644 --- a/MMEX/Data/CategoryData.swift +++ b/MMEX/Data/CategoryData.swift @@ -13,12 +13,19 @@ struct CategoryData: ExportableEntity { var name : String = "" var active : Bool = false var parentId : DataId = 0 + + var parentCategories: [CategoryData] = [] } extension CategoryData { var isRoot: Bool { return parentId <= 0 } + func fullName(with delimiter: String = ":") -> String { + // Join all parent category names followed by the current category's name + let parentNames = parentCategories.map { $0.name } + return (parentNames + [name]).joined(separator: delimiter) + } } extension CategoryData: DataProtocol { diff --git a/MMEX/Data/InfotableData.swift b/MMEX/Data/InfotableData.swift index 935250b0..82f5a57e 100644 --- a/MMEX/Data/InfotableData.swift +++ b/MMEX/Data/InfotableData.swift @@ -15,6 +15,7 @@ enum InfoKey: String { case dateFormat = "DATEFORMAT" case createDate = "CREATEDATE" case uid = "UID" + case categDelimiter = "CATEG_DELIMITER" var id: String { return rawValue } } @@ -57,5 +58,6 @@ extension InfotableData { InfotableData(id: 2, name: InfoKey.createDate.id, value: DateString(Date()).string), InfotableData(id: 3, name: InfoKey.baseCurrencyID.id, value: "1"), InfotableData(id: 4, name: InfoKey.defaultAccountID.id, value: "1"), + InfotableData(id: 5, name: InfoKey.categDelimiter.id, value: ":"), ] } diff --git a/MMEX/View/Enter/EnterView.swift b/MMEX/View/Enter/EnterView.swift index 2f3add6e..981a50b5 100644 --- a/MMEX/View/Enter/EnterView.swift +++ b/MMEX/View/Enter/EnterView.swift @@ -17,14 +17,14 @@ struct EnterView: View { @Environment(\.dismiss) var dismiss @State private var accountId: [DataId] = [] - @State private var categories: [CategoryData] = [] @State private var payees: [PayeeData] = [] var body: some View { NavigationStack { TransactionEditView( + viewModel: viewModel, accountId: $accountId, - categories: $categories, + categories: $viewModel.categories, payees: $payees, txn: $newTxn ) @@ -51,7 +51,7 @@ struct EnterView: View { // .navigationBarTitle("Add Transaction", displayMode: .inline) .onAppear() { loadAccounts() - loadCategories() + viewModel.loadCategories() loadPayees() // TODO update initial payee (e.g. last used) // TODO update category, payee associated? @@ -85,13 +85,4 @@ struct EnterView: View { } } } - - func loadCategories() { - DispatchQueue.global(qos: .background).async { - let loadedCategories = env.categoryRepository?.load() ?? [] - DispatchQueue.main.async { - self.categories = loadedCategories - } - } - } } diff --git a/MMEX/View/Settings/SettingsView.swift b/MMEX/View/Settings/SettingsView.swift index 166ce48e..61312c7e 100644 --- a/MMEX/View/Settings/SettingsView.swift +++ b/MMEX/View/Settings/SettingsView.swift @@ -64,6 +64,11 @@ struct SettingsView: View { Spacer() Text("\(dateFormat)") } + HStack { + Text("Category Delimiter") + Spacer() + Text("\(viewModel.categDelimiter)") + } Picker("Base Currency", selection: $viewModel.baseCurrencyId) { ForEach(viewModel.currencies) { currency in HStack { diff --git a/MMEX/View/Transaction/TransactionAddView.swift b/MMEX/View/Transaction/TransactionAddView.swift index 70d3e48b..17374a65 100644 --- a/MMEX/View/Transaction/TransactionAddView.swift +++ b/MMEX/View/Transaction/TransactionAddView.swift @@ -8,6 +8,7 @@ import SwiftUI struct TransactionAddView: View { + @ObservedObject var viewModel: TransactionViewModel @Binding var accountId: [DataId] @Binding var categories: [CategoryData] @Binding var payees: [PayeeData] @@ -19,6 +20,7 @@ struct TransactionAddView: View { var body: some View { NavigationStack { TransactionEditView( + viewModel: viewModel, accountId: $accountId, categories: $categories, payees: $payees, diff --git a/MMEX/View/Transaction/TransactionDetailView.swift b/MMEX/View/Transaction/TransactionDetailView.swift index 0d6ae93d..2a49d17d 100644 --- a/MMEX/View/Transaction/TransactionDetailView.swift +++ b/MMEX/View/Transaction/TransactionDetailView.swift @@ -131,6 +131,7 @@ struct TransactionDetailView: View { .sheet(isPresented: $isPresentingEditView) { NavigationStack { TransactionEditView( + viewModel: viewModel, accountId: $accountId, categories: $categories, payees: $payees, diff --git a/MMEX/View/Transaction/TransactionEditView.swift b/MMEX/View/Transaction/TransactionEditView.swift index 8df2ed56..c9e56125 100644 --- a/MMEX/View/Transaction/TransactionEditView.swift +++ b/MMEX/View/Transaction/TransactionEditView.swift @@ -9,6 +9,7 @@ import SwiftUI struct TransactionEditView: View { @EnvironmentObject var env: EnvironmentManager // Access EnvironmentManager + @ObservedObject var viewModel: TransactionViewModel @Binding var accountId: [DataId] // sorted by name @Binding var categories: [CategoryData] @Binding var payees: [PayeeData] @@ -143,7 +144,7 @@ struct TransactionEditView: View { Text("Category").tag(0 as Int64) // not set } ForEach(categories) { category in - Text(category.name).tag(category.id) + Text(category.fullName(with: viewModel.categDelimiter)).tag(category.id) } } .pickerStyle(MenuPickerStyle()) // Show a menu for the category picker @@ -277,6 +278,7 @@ struct TransactionEditView: View { #Preview("txn 0") { TransactionEditView( + viewModel: TransactionViewModel(env: EnvironmentManager.sampleData), accountId: .constant(AccountData.sampleDataIds), categories: .constant(CategoryData.sampleData), payees: .constant(PayeeData.sampleData), @@ -287,6 +289,7 @@ struct TransactionEditView: View { #Preview("txn 3") { TransactionEditView( + viewModel: TransactionViewModel(env: EnvironmentManager.sampleData), accountId: .constant(AccountData.sampleDataIds), categories: .constant(CategoryData.sampleData), payees: .constant(PayeeData.sampleData), diff --git a/MMEX/View/Transaction/TransactionListView.swift b/MMEX/View/Transaction/TransactionListView.swift index 06af7094..62aedc89 100644 --- a/MMEX/View/Transaction/TransactionListView.swift +++ b/MMEX/View/Transaction/TransactionListView.swift @@ -97,6 +97,7 @@ struct TransactionListView: View { } .sheet(isPresented: $isPresentingTransactionAddView) { TransactionAddView( + viewModel: viewModel, accountId: $viewModel.accountId, categories: $viewModel.categories, payees: $viewModel.payees, diff --git a/MMEX/ViewModel/TransactionViewModel.swift b/MMEX/ViewModel/TransactionViewModel.swift index 72849563..697a085e 100644 --- a/MMEX/ViewModel/TransactionViewModel.swift +++ b/MMEX/ViewModel/TransactionViewModel.swift @@ -16,6 +16,7 @@ class TransactionViewModel: ObservableObject { @Published var defaultAccountId: DataId = 0 @Published var baseCurrency: CurrencyData? @Published var defaultAccount: AccountData? + @Published var categDelimiter: String = ":" private var cancellables = Set() @@ -71,6 +72,10 @@ class TransactionViewModel: ObservableObject { from: AccountRepository.table.filter(AccountRepository.col_id == Int64(defaultAccountId)) ).toOptional() } + + if let categDelimiter = infotableRepo?.getValue(for: InfoKey.categDelimiter.id, as: String.self) { + self.categDelimiter = categDelimiter + } } // Set up individual bindings for each @Published property @@ -120,13 +125,39 @@ class TransactionViewModel: ObservableObject { } } + private func populateParentCategories(for categories: [CategoryData]) -> [CategoryData] { + // Create a dictionary for quick parent lookups + let categoryDict = Dictionary(uniqueKeysWithValues: categories.map { ($0.id, $0) }) + + return categories.map { category in + var updatedCategory = category + updatedCategory.parentCategories = self.findParentCategories(for: category, in: categoryDict) + return updatedCategory + } + } + + private func findParentCategories(for category: CategoryData, in categoryDict: [DataId: CategoryData]) -> [CategoryData] { + // Recursive function to find all parent categories + var parents: [CategoryData] = [] + + var currentCategory = category + while let parentCategory = categoryDict[currentCategory.parentId], parentCategory.id != 0 { + parents.insert(parentCategory, at: 0) // Insert at the beginning to maintain the correct order + currentCategory = parentCategory + } + + return parents + } + func loadCategories() { let repository = env.categoryRepository DispatchQueue.global(qos: .background).async { let loadedCategories = repository?.load() ?? [] - let loadedCategoryDict = Dictionary(uniqueKeysWithValues: loadedCategories.map { ($0.id, $0) }) + let updatedCategories = self.populateParentCategories(for: loadedCategories) + + let loadedCategoryDict = Dictionary(uniqueKeysWithValues: updatedCategories.map { ($0.id, $0) }) DispatchQueue.main.async { - self.categories = loadedCategories + self.categories = updatedCategories self.categoryDict = loadedCategoryDict } }