204 lines
5.8 KiB
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
|
|
}
|
|
}
|