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 inputMode: InputMode = .simple @Published var currencySymbol = "€" @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 let settings = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext) if let accountCurrency = source.account?.currency, !accountCurrency.isEmpty { currencySymbol = CurrencyFormatter.symbol(for: accountCurrency) } else { currencySymbol = settings.currencySymbol } if let accountMode = InputMode(rawValue: source.account?.inputMode ?? "") { inputMode = accountMode } else { inputMode = InputMode(rawValue: settings.inputMode) ?? .simple } includeContribution = inputMode == .detailed 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: currencySymbol, 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 CurrencyFormatter.format(Decimal.zero) } 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: - Duplicate Previous func prefillFromPreviousSnapshot() { guard case .add = mode, let previous = source.latestSnapshot else { return } valueString = formatDecimalForInput(previous.decimalValue) if let contribution = previous.contribution { includeContribution = true contributionString = formatDecimalForInput(contribution.decimalValue) } notes = previous.notes ?? "" date = Date() } // MARK: - Date Validation var isDateInFuture: Bool { date > Date() } var dateWarning: String? { if isDateInFuture { return "Date is in the future" } return nil } }