129 lines
4.6 KiB
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
|
|
}
|
|
}
|
|
}
|