import Foundation import Combine import CoreData @MainActor class DashboardViewModel: ObservableObject { // MARK: - Published Properties @Published var portfolioSummary: PortfolioSummary = .empty @Published var categoryMetrics: [CategoryMetrics] = [] @Published var recentSnapshots: [Snapshot] = [] @Published var sourcesNeedingUpdate: [InvestmentSource] = [] @Published var isLoading = false @Published var errorMessage: String? // MARK: - Chart Data @Published var evolutionData: [(date: Date, value: Decimal)] = [] // MARK: - Dependencies private let categoryRepository: CategoryRepository private let sourceRepository: InvestmentSourceRepository private let snapshotRepository: SnapshotRepository private let calculationService: CalculationService private var cancellables = Set() // MARK: - Initialization init( categoryRepository: CategoryRepository = CategoryRepository(), sourceRepository: InvestmentSourceRepository = InvestmentSourceRepository(), snapshotRepository: SnapshotRepository = SnapshotRepository(), calculationService: CalculationService = .shared ) { self.categoryRepository = categoryRepository self.sourceRepository = sourceRepository self.snapshotRepository = snapshotRepository self.calculationService = calculationService setupObservers() loadData() } // MARK: - Setup private func setupObservers() { // Observe category changes categoryRepository.$categories .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.refreshData() } .store(in: &cancellables) // Observe source changes sourceRepository.$sources .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.refreshData() } .store(in: &cancellables) // Observe Core Data changes NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange) .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) .sink { [weak self] _ in self?.refreshData() } .store(in: &cancellables) } // MARK: - Data Loading func loadData() { isLoading = true errorMessage = nil Task { await refreshAllData() isLoading = false } } func refreshData() { Task { await refreshAllData() } } private func refreshAllData() async { let categories = categoryRepository.categories let sources = sourceRepository.sources let allSnapshots = snapshotRepository.fetchAllSnapshots() // Calculate portfolio summary portfolioSummary = calculationService.calculatePortfolioSummary( from: sources, snapshots: allSnapshots ) // Calculate category metrics categoryMetrics = calculationService.calculateCategoryMetrics( for: categories, totalPortfolioValue: portfolioSummary.totalValue ).sorted { $0.totalValue > $1.totalValue } // Get recent snapshots recentSnapshots = Array(allSnapshots.prefix(10)) // Get sources needing update sourcesNeedingUpdate = sourceRepository.fetchSourcesNeedingUpdate() // Calculate evolution data for chart calculateEvolutionData(from: allSnapshots) // Log screen view FirebaseService.shared.logScreenView(screenName: "Dashboard") } private func calculateEvolutionData(from snapshots: [Snapshot]) { // Group snapshots by date and sum values var dateValues: [Date: Decimal] = [:] // Get unique dates let sortedSnapshots = snapshots.sorted { $0.date < $1.date } for snapshot in sortedSnapshots { let startOfDay = Calendar.current.startOfDay(for: snapshot.date) // For each date, we need the total portfolio value at that point // This requires summing the latest value for each source up to that date let sourcesAtDate = Dictionary(grouping: sortedSnapshots.filter { $0.date <= snapshot.date }) { $0.source?.id ?? UUID() } var totalAtDate: Decimal = 0 for (_, sourceSnapshots) in sourcesAtDate { if let latestForSource = sourceSnapshots.max(by: { $0.date < $1.date }) { totalAtDate += latestForSource.decimalValue } } dateValues[startOfDay] = totalAtDate } evolutionData = dateValues .map { (date: $0.key, value: $0.value) } .sorted { $0.date < $1.date } } // MARK: - Computed Properties var hasData: Bool { !sourceRepository.sources.isEmpty } var totalSourceCount: Int { sourceRepository.sourceCount } var totalCategoryCount: Int { categoryRepository.categories.count } var pendingUpdatesCount: Int { sourcesNeedingUpdate.count } var topCategories: [CategoryMetrics] { Array(categoryMetrics.prefix(5)) } // MARK: - Formatting var formattedTotalValue: String { portfolioSummary.formattedTotalValue } var formattedDayChange: String { portfolioSummary.formattedDayChange } var formattedMonthChange: String { portfolioSummary.formattedMonthChange } var formattedYearChange: String { portfolioSummary.formattedYearChange } var isDayChangePositive: Bool { portfolioSummary.dayChange >= 0 } var isMonthChangePositive: Bool { portfolioSummary.monthChange >= 0 } var isYearChangePositive: Bool { portfolioSummary.yearChange >= 0 } }