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 @State private var selectedCategoryId: UUID? @StateObject private var categoryRepository = CategoryRepository() 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) .foregroundStyle(.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") } Section { Picker(String(localized: "goal_scope_category"), selection: $selectedCategoryId) { Text("goal_scope_all_categories").tag(UUID?.none) ForEach(categoryRepository.categories) { category in Label(category.name, systemImage: category.icon) .tag(UUID?.some(category.id)) } } } header: { Text("goal_scope_section") } footer: { Text(selectedCategoryId == nil ? String(localized: "goal_scope_footer_all") : String(localized: "goal_scope_footer_category")) } } .navigationTitle(goal == nil ? "New Goal" : "Edit Goal") .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .topBarTrailing) { 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 } selectedCategoryId = goal.category?.safeId didLoadGoal = true } } private var isValid: Bool { !name.trimmingCharacters(in: .whitespaces).isEmpty && parseDecimal(targetAmount) != nil } private var selectedCategory: Category? { guard let selectedCategoryId else { return nil } return categoryRepository.categories.first { $0.safeId == selectedCategoryId } } private func saveGoal() { guard let value = parseDecimal(targetAmount) else { return } let category = selectedCategory if let goal { goalRepository.updateGoal( goal, name: name, targetAmount: value, targetDate: includeTargetDate ? targetDate : nil, clearTargetDate: !includeTargetDate, category: category, clearCategory: category == nil ) } else { goalRepository.createGoal( name: name, targetAmount: value, targetDate: includeTargetDate ? targetDate : nil, account: account, category: category ) } 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) }