import SwiftUI struct AddSourceView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject var iapService: IAPService @EnvironmentObject var accountStore: AccountStore @State private var name = "" @State private var selectedCategory: Category? @State private var initialValue = "" @State private var showingCategoryPicker = false @State private var selectedAccountId: UUID? @StateObject private var categoryRepository = CategoryRepository() var body: some View { NavigationStack { Form { if accountStore.accounts.count > 1 { Section { Picker("Account", selection: $selectedAccountId) { ForEach(accountStore.accounts) { account in Text(account.name).tag(Optional(account.id)) } } } header: { Text("Account") } } // Source Info Section { TextField("Source Name", text: $name) .textContentType(.organizationName) Button { showingCategoryPicker = true } label: { HStack { Text("Category") .foregroundColor(.primary) Spacer() if let category = selectedCategory { HStack(spacing: 6) { Image(systemName: category.icon) .foregroundColor(category.color) Text(category.name) .foregroundColor(.secondary) } } else { Text("Select") .foregroundColor(.secondary) } Image(systemName: "chevron.right") .font(.caption) .foregroundColor(.secondary) } } } header: { Text("Source Information") } // Initial Value (Optional) Section { HStack { Text(currencySymbol) .foregroundColor(.secondary) TextField("0.00", text: $initialValue) .keyboardType(.decimalPad) } } header: { Text("Initial Value (Optional)") } footer: { Text("You can add snapshots later if you prefer.") } } .navigationTitle("Add Source") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Add") { saveSource() } .disabled(!isValid) .fontWeight(.semibold) } } .sheet(isPresented: $showingCategoryPicker) { CategoryPickerView( selectedCategory: $selectedCategory, categories: categoryRepository.categories ) } .onAppear { categoryRepository.createDefaultCategoriesIfNeeded() if selectedCategory == nil { selectedCategory = categoryRepository.categories.first } if selectedAccountId == nil { selectedAccountId = accountStore.selectedAccount?.id ?? accountStore.accounts.first?.id } } } } // MARK: - Validation private var isValid: Bool { !name.trimmingCharacters(in: .whitespaces).isEmpty && selectedCategory != nil } // MARK: - Save private func saveSource() { guard let category = selectedCategory else { return } let repository = InvestmentSourceRepository() let source = repository.createSource( name: name.trimmingCharacters(in: .whitespaces), category: category, account: selectedAccount ) // Add initial snapshot if provided if let value = parseDecimal(initialValue), value > 0 { let snapshotRepository = SnapshotRepository() snapshotRepository.createSnapshot( for: source, date: Date(), value: value ) } // Schedule notification NotificationService.shared.scheduleReminder(for: source) // Log analytics FirebaseService.shared.logSourceAdded( categoryName: category.name, sourceCount: repository.sourceCount ) dismiss() } 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 var currencySymbol: String { if let account = selectedAccount, let code = account.currencyCode { return CurrencyFormatter.symbol(for: code) } return AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currencySymbol } private var selectedAccount: Account? { guard let id = selectedAccountId else { return nil } return accountStore.accounts.first { $0.id == id } } } // MARK: - Category Picker View struct CategoryPickerView: View { @Environment(\.dismiss) private var dismiss @Binding var selectedCategory: Category? let categories: [Category] var body: some View { NavigationStack { List(categories) { category in Button { selectedCategory = category dismiss() } label: { HStack(spacing: 12) { ZStack { Circle() .fill(category.color.opacity(0.2)) .frame(width: 40, height: 40) Image(systemName: category.icon) .foregroundColor(category.color) } Text(category.name) .foregroundColor(.primary) Spacer() if selectedCategory?.id == category.id { Image(systemName: "checkmark") .foregroundColor(.appPrimary) } } } } .navigationTitle("Select Category") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Done") { dismiss() } } } } } } // MARK: - Edit Source View struct EditSourceView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject var accountStore: AccountStore let source: InvestmentSource @State private var name: String @State private var selectedCategory: Category? @State private var showingCategoryPicker = false @State private var selectedAccountId: UUID? @StateObject private var categoryRepository = CategoryRepository() init(source: InvestmentSource) { self.source = source _name = State(initialValue: source.name) _selectedCategory = State(initialValue: source.category) _selectedAccountId = State(initialValue: source.account?.id) } var body: some View { NavigationStack { Form { if accountStore.accounts.count > 1 { Section { Picker("Account", selection: $selectedAccountId) { ForEach(accountStore.accounts) { account in Text(account.name).tag(Optional(account.id)) } } } header: { Text("Account") } } Section { TextField("Source Name", text: $name) Button { showingCategoryPicker = true } label: { HStack { Text("Category") .foregroundColor(.primary) Spacer() if let category = selectedCategory { HStack(spacing: 6) { Image(systemName: category.icon) .foregroundColor(category.color) Text(category.name) .foregroundColor(.secondary) } } Image(systemName: "chevron.right") .font(.caption) .foregroundColor(.secondary) } } } } .navigationTitle("Edit Source") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { saveChanges() } .disabled(!isValid) .fontWeight(.semibold) } } .sheet(isPresented: $showingCategoryPicker) { CategoryPickerView( selectedCategory: $selectedCategory, categories: categoryRepository.categories ) } } } private var isValid: Bool { !name.trimmingCharacters(in: .whitespaces).isEmpty && selectedCategory != nil } private func saveChanges() { guard let category = selectedCategory else { return } let repository = InvestmentSourceRepository() repository.updateSource( source, name: name.trimmingCharacters(in: .whitespaces), category: category, account: selectedAccount ) // Reschedule notification NotificationService.shared.scheduleReminder(for: source) dismiss() } private var selectedAccount: Account? { guard let id = selectedAccountId else { return nil } return accountStore.accounts.first { $0.id == id } } } // MARK: - Add Snapshot View struct AddSnapshotView: View { @Environment(\.dismiss) private var dismiss let source: InvestmentSource let snapshot: Snapshot? @StateObject private var viewModel: SnapshotFormViewModel init(source: InvestmentSource, snapshot: Snapshot? = nil) { self.source = source self.snapshot = snapshot if let snapshot = snapshot { _viewModel = StateObject(wrappedValue: SnapshotFormViewModel(source: source, mode: .edit(snapshot))) } else { _viewModel = StateObject(wrappedValue: SnapshotFormViewModel(source: source)) } } var body: some View { NavigationStack { Form { Section { if snapshot == nil && viewModel.previousValue != nil { Button { viewModel.prefillFromPreviousSnapshot() } label: { Label("Duplicate Previous Snapshot", systemImage: "doc.on.doc") } } DatePicker( "Date", selection: $viewModel.date, in: ...Date(), displayedComponents: .date ) HStack { Text(viewModel.currencySymbol) .foregroundColor(.secondary) TextField("Value", text: $viewModel.valueString) .keyboardType(.decimalPad) } if viewModel.previousValue != nil { Text(viewModel.previousValueString) .font(.caption) .foregroundColor(.secondary) } } header: { Text("Snapshot Details") } // Change preview if let change = viewModel.formattedChange, let percentage = viewModel.formattedChangePercentage { Section { HStack { Text("Change from previous") Spacer() Text("\(change) (\(percentage))") .foregroundColor( (viewModel.changeFromPrevious ?? 0) >= 0 ? .positiveGreen : .negativeRed ) } } } if viewModel.inputMode == .detailed { Section { Toggle("Include Contribution", isOn: $viewModel.includeContribution) if viewModel.includeContribution { HStack { Text(viewModel.currencySymbol) .foregroundColor(.secondary) TextField("New capital added", text: $viewModel.contributionString) .keyboardType(.decimalPad) } } } header: { Text("Contribution (Optional)") } footer: { Text("Track new capital you've added to separate it from investment growth.") } } Section { TextField("Notes", text: $viewModel.notes, axis: .vertical) .lineLimit(3...6) } header: { Text("Notes (Optional)") } } .navigationTitle(viewModel.title) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button(viewModel.buttonTitle) { saveSnapshot() } .disabled(!viewModel.isValid) .fontWeight(.semibold) } } } } private func saveSnapshot() { guard let value = viewModel.value else { return } let repository = SnapshotRepository() if let snapshot = snapshot { repository.updateSnapshot( snapshot, date: viewModel.date, value: value, contribution: viewModel.contribution, notes: viewModel.notes.isEmpty ? nil : viewModel.notes, clearContribution: !viewModel.includeContribution, clearNotes: viewModel.notes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ) } else { repository.createSnapshot( for: source, date: viewModel.date, value: value, contribution: viewModel.contribution, notes: viewModel.notes.isEmpty ? nil : viewModel.notes ) } // Reschedule notification NotificationService.shared.scheduleReminder(for: source) // Log analytics FirebaseService.shared.logSnapshotAdded(sourceName: source.name, value: value) dismiss() } } #Preview { AddSourceView() .environmentObject(IAPService()) .environmentObject(AccountStore(iapService: IAPService())) }