initial version
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
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<AnyCancellable>()
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user