InvestmentTrackerApp/InvestmentTracker/ViewModels/DashboardViewModel.swift

204 lines
5.8 KiB
Swift

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<AnyCancellable>()
// 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
}
}