InvestmentTrackerApp/PortfolioJournal/ViewModels/MonthlyCheckInViewModel.swift

129 lines
4.6 KiB
Swift

import Foundation
import Combine
import CoreData
@MainActor
class MonthlyCheckInViewModel: ObservableObject {
@Published var sourcesNeedingUpdate: [InvestmentSource] = []
@Published var sources: [InvestmentSource] = []
@Published var monthlySummary: MonthlySummary = .empty
@Published var recentNotes: [Snapshot] = []
@Published var isLoading = false
@Published var selectedAccount: Account?
@Published var showAllAccounts = true
@Published var selectedRange: DateRange = .thisMonth
@Published var checkInStats: MonthlyCheckInStats = .empty
private let sourceRepository: InvestmentSourceRepository
private let snapshotRepository: SnapshotRepository
private let calculationService: CalculationService
private var cancellables = Set<AnyCancellable>()
@MainActor
init(
sourceRepository: InvestmentSourceRepository? = nil,
snapshotRepository: SnapshotRepository? = nil,
calculationService: CalculationService? = nil
) {
let context = CoreDataStack.shared.viewContext
self.sourceRepository = sourceRepository ?? InvestmentSourceRepository(context: context)
self.snapshotRepository = snapshotRepository ?? SnapshotRepository(context: context)
self.calculationService = calculationService ?? .shared
setupObservers()
refresh()
}
private func setupObservers() {
sourceRepository.$sources
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.refresh()
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange)
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [weak self] _ in
self?.refresh()
}
.store(in: &cancellables)
}
func refresh() {
isLoading = true
let filtered = filteredSources()
let snapshots = filteredSnapshots(for: filtered)
let accountFilter = showAllAccounts ? nil : selectedAccount
sourcesNeedingUpdate = sourceRepository.fetchSourcesNeedingUpdate(for: accountFilter)
sources = filtered
monthlySummary = calculationService.calculateMonthlySummary(
sources: filtered,
snapshots: snapshots,
range: selectedRange
)
checkInStats = MonthlyCheckInStore.stats(referenceDate: selectedRange.end)
recentNotes = snapshots
.filter { ($0.notes?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
&& selectedRange.contains($0.date) }
.sorted { $0.date > $1.date }
.prefix(5)
.map { $0 }
isLoading = false
}
func duplicatePreviousMonthSnapshots(referenceDate: Date) {
let targetRange = DateRange.month(containing: referenceDate)
let previousRange = DateRange.month(containing: referenceDate.adding(months: -1))
let sources = filteredSources()
guard !sources.isEmpty else { return }
let targetSnapshots = snapshotRepository.fetchSnapshots(
from: targetRange.start,
to: targetRange.end
)
let targetSourceIds = Set(targetSnapshots.compactMap { $0.source?.id })
let now = Date()
let targetDate = targetRange.contains(now) ? now : targetRange.end
for source in sources {
let sourceId = source.id
if targetSourceIds.contains(sourceId) { continue }
let previousSnapshots = snapshotRepository.fetchSnapshots(for: source)
guard let previousSnapshot = previousSnapshots.first(where: { previousRange.contains($0.date) }) else {
continue
}
snapshotRepository.createSnapshot(
for: source,
date: targetDate,
value: previousSnapshot.decimalValue,
contribution: previousSnapshot.contribution?.decimalValue,
notes: previousSnapshot.notes
)
}
}
private func filteredSources() -> [InvestmentSource] {
if showAllAccounts || selectedAccount == nil {
return sourceRepository.sources
}
return sourceRepository.sources.filter { $0.account?.id == selectedAccount?.id }
}
private func filteredSnapshots(for sources: [InvestmentSource]) -> [Snapshot] {
let sourceIds = Set(sources.compactMap { $0.id })
return snapshotRepository.fetchAllSnapshots().filter { snapshot in
let sourceId = snapshot.source?.id
return sourceId.map(sourceIds.contains) ?? false
}
}
}