import SwiftUI struct GoalEditorView: View { @Environment(\.dismiss) private var dismiss @State private var name = "" @State private var targetAmount = "" @State private var targetDate = Date() @State private var includeTargetDate = false @State private var didLoadGoal = false let account: Account? let goal: Goal? private let goalRepository = GoalRepository() init(account: Account?, goal: Goal? = nil) { self.account = account self.goal = goal } private var currencySymbol: String { if let account = account, let code = account.currency, !code.isEmpty { return CurrencyFormatter.symbol(for: code) } return AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currencySymbol } private var currencyCode: String { if let account = account, let code = account.currency, !code.isEmpty { return code } return AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency } var body: some View { NavigationStack { Form { Section { TextField("Goal name", text: $name) HStack { Text(currencySymbol) .foregroundColor(.secondary) TextField("Target amount", text: $targetAmount) .keyboardType(.decimalPad) } Toggle("Add target date", isOn: $includeTargetDate) if includeTargetDate { DatePicker("Target date", selection: $targetDate, displayedComponents: .date) .datePickerStyle(.graphical) } } header: { Text("Goal Details") } } .navigationTitle(goal == nil ? "New Goal" : "Edit Goal") .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { saveGoal() } .disabled(!isValid) } } } .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) .onAppear { guard let goal, !didLoadGoal else { return } name = goal.name ?? "" if let amount = goal.targetAmount?.decimalValue { targetAmount = formatDecimalForInput(amount) } if let target = goal.targetDate { includeTargetDate = true targetDate = target } didLoadGoal = true } } private var isValid: Bool { !name.trimmingCharacters(in: .whitespaces).isEmpty && parseDecimal(targetAmount) != nil } private func saveGoal() { guard let value = parseDecimal(targetAmount) else { return } if let goal { goalRepository.updateGoal( goal, name: name, targetAmount: value, targetDate: includeTargetDate ? targetDate : nil, clearTargetDate: !includeTargetDate ) } else { goalRepository.createGoal( name: name, targetAmount: value, targetDate: includeTargetDate ? targetDate : nil, account: account ) } dismiss() } private func parseDecimal(_ value: String) -> Decimal? { let locale = CurrencyFormatter.locale(for: currencyCode) let stripped = value .replacingOccurrences(of: currencySymbol, with: "") .trimmingCharacters(in: .whitespaces) guard !stripped.isEmpty else { return nil } let decimalSep = locale.decimalSeparator ?? "." let groupingSep = locale.groupingSeparator ?? "" // Detect alternate decimal BEFORE removing separators let usesAlternateDecimal = (decimalSep == "," && stripped.contains(".") && !stripped.contains(",")) || (decimalSep == "." && stripped.contains(",") && !stripped.contains(".")) if usesAlternateDecimal { let normalized = stripped .replacingOccurrences(of: groupingSep, with: "") .replacingOccurrences(of: ",", with: ".") let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.locale = Locale(identifier: "en_US_POSIX") return formatter.number(from: normalized)?.decimalValue } let cleaned = stripped.replacingOccurrences(of: groupingSep, with: "") let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.locale = locale if let result = formatter.number(from: cleaned)?.decimalValue { return result } // Fallback for mixed locale input let normalized = cleaned .replacingOccurrences(of: decimalSep, with: ".") .replacingOccurrences(of: ",", with: ".") formatter.locale = Locale(identifier: "en_US_POSIX") return formatter.number(from: normalized)?.decimalValue } private func formatDecimalForInput(_ decimal: Decimal) -> String { let locale = CurrencyFormatter.locale(for: currencyCode) let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.locale = locale formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 2 formatter.groupingSeparator = "" return formatter.string(from: decimal as NSDecimalNumber) ?? "" } } #Preview { GoalEditorView(account: nil) }