import Foundation import Combine @MainActor class SnapshotFormViewModel: ObservableObject { // MARK: - Published Properties @Published var date = Date() @Published var valueString = "" @Published var contributionString = "" @Published var notes = "" @Published var includeContribution = false @Published var isValid = false @Published var errorMessage: String? // MARK: - Mode enum Mode { case add case edit(Snapshot) } let mode: Mode let source: InvestmentSource // MARK: - Dependencies private var cancellables = Set() // MARK: - Initialization init(source: InvestmentSource, mode: Mode = .add) { self.source = source self.mode = mode setupValidation() // Pre-fill for edit mode if case .edit(let snapshot) = mode { date = snapshot.date valueString = formatDecimalForInput(snapshot.decimalValue) if let contribution = snapshot.contribution { includeContribution = true contributionString = formatDecimalForInput(contribution.decimalValue) } notes = snapshot.notes ?? "" } } // MARK: - Validation private func setupValidation() { Publishers.CombineLatest3($valueString, $contributionString, $includeContribution) .map { [weak self] valueStr, contribStr, includeContrib in self?.validateInputs( valueString: valueStr, contributionString: contribStr, includeContribution: includeContrib ) ?? false } .assign(to: &$isValid) } private func validateInputs( valueString: String, contributionString: String, includeContribution: Bool ) -> Bool { // Value is required guard let value = parseDecimal(valueString), value >= 0 else { return false } // Contribution is optional but must be valid if included if includeContribution { guard let contribution = parseDecimal(contributionString), contribution >= 0 else { return false } } return true } // MARK: - Parsing private func parseDecimal(_ string: String) -> Decimal? { let cleaned = string .replacingOccurrences(of: "€", with: "") .replacingOccurrences(of: ",", with: ".") .trimmingCharacters(in: .whitespaces) guard !cleaned.isEmpty else { return nil } let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.locale = Locale(identifier: "en_US") return formatter.number(from: cleaned)?.decimalValue } private func formatDecimalForInput(_ decimal: Decimal) -> String { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.minimumFractionDigits = 2 formatter.maximumFractionDigits = 2 formatter.groupingSeparator = "" return formatter.string(from: decimal as NSDecimalNumber) ?? "" } // MARK: - Computed Properties var value: Decimal? { parseDecimal(valueString) } var contribution: Decimal? { guard includeContribution else { return nil } return parseDecimal(contributionString) } var formattedValue: String { guard let value = value else { return "€0.00" } return value.currencyString } var title: String { switch mode { case .add: return "Add Snapshot" case .edit: return "Edit Snapshot" } } var buttonTitle: String { switch mode { case .add: return "Add Snapshot" case .edit: return "Save Changes" } } // MARK: - Previous Value Reference var previousValue: Decimal? { source.latestSnapshot?.decimalValue } var previousValueString: String { guard let previous = previousValue else { return "No previous value" } return "Previous: \(previous.currencyString)" } var changeFromPrevious: Decimal? { guard let current = value, let previous = previousValue else { return nil } return current - previous } var changePercentageFromPrevious: Double? { guard let current = value, let previous = previousValue, previous != 0 else { return nil } return NSDecimalNumber(decimal: (current - previous) / previous).doubleValue * 100 } var formattedChange: String? { guard let change = changeFromPrevious else { return nil } let prefix = change >= 0 ? "+" : "" return "\(prefix)\(change.currencyString)" } var formattedChangePercentage: String? { guard let percentage = changePercentageFromPrevious else { return nil } let prefix = percentage >= 0 ? "+" : "" return String(format: "\(prefix)%.2f%%", percentage) } // MARK: - Date Validation var isDateInFuture: Bool { date > Date() } var dateWarning: String? { if isDateInFuture { return "Date is in the future" } return nil } }