import Foundation import CoreData import Combine class CategoryRepository: ObservableObject { private let context: NSManagedObjectContext @Published private(set) var categories: [Category] = [] init(context: NSManagedObjectContext = CoreDataStack.shared.viewContext) { self.context = context fetchCategories() setupNotificationObserver() } private func setupNotificationObserver() { NotificationCenter.default.addObserver( self, selector: #selector(contextDidChange), name: .NSManagedObjectContextObjectsDidChange, object: context ) } @objc private func contextDidChange(_ notification: Notification) { fetchCategories() } // MARK: - Fetch func fetchCategories() { let request: NSFetchRequest = Category.fetchRequest() request.sortDescriptors = [ NSSortDescriptor(keyPath: \Category.sortOrder, ascending: true), NSSortDescriptor(keyPath: \Category.name, ascending: true) ] do { categories = try context.fetch(request) } catch { print("Failed to fetch categories: \(error)") categories = [] } } func fetchCategory(by id: UUID) -> Category? { let request: NSFetchRequest = Category.fetchRequest() request.predicate = NSPredicate(format: "id == %@", id as CVarArg) request.fetchLimit = 1 return try? context.fetch(request).first } // MARK: - Create @discardableResult func createCategory( name: String, colorHex: String, icon: String ) -> Category { let category = Category(context: context) category.name = name category.colorHex = colorHex category.icon = icon category.sortOrder = Int16(categories.count) save() return category } func createDefaultCategoriesIfNeeded() { guard categories.isEmpty else { return } Category.createDefaultCategories(in: context) fetchCategories() } // MARK: - Update func updateCategory( _ category: Category, name: String? = nil, colorHex: String? = nil, icon: String? = nil ) { if let name = name { category.name = name } if let colorHex = colorHex { category.colorHex = colorHex } if let icon = icon { category.icon = icon } save() } func updateSortOrder(_ categories: [Category]) { for (index, category) in categories.enumerated() { category.sortOrder = Int16(index) } save() } // MARK: - Delete func deleteCategory(_ category: Category) { context.delete(category) save() } func deleteCategory(at offsets: IndexSet) { for index in offsets { guard index < categories.count else { continue } context.delete(categories[index]) } save() } // MARK: - Statistics var totalValue: Decimal { categories.reduce(Decimal.zero) { $0 + $1.totalValue } } func categoryAllocation() -> [(category: Category, percentage: Double)] { let total = totalValue guard total > 0 else { return [] } return categories.map { category in let percentage = NSDecimalNumber(decimal: category.totalValue / total).doubleValue * 100 return (category: category, percentage: percentage) }.sorted { $0.percentage > $1.percentage } } // MARK: - Save private func save() { guard context.hasChanges else { return } do { try context.save() fetchCategories() } catch { print("Failed to save context: \(error)") } } }