InvestmentTrackerApp/InvestmentTracker/Views/Sources/AddSourceView.swift

477 lines
16 KiB
Swift

import SwiftUI
struct AddSourceView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject var iapService: IAPService
@State private var name = ""
@State private var selectedCategory: Category?
@State private var notificationFrequency: NotificationFrequency = .monthly
@State private var customFrequencyMonths = 1
@State private var initialValue = ""
@State private var showingCategoryPicker = false
@StateObject private var categoryRepository = CategoryRepository()
var body: some View {
NavigationStack {
Form {
// 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("")
.foregroundColor(.secondary)
TextField("0.00", text: $initialValue)
.keyboardType(.decimalPad)
}
} header: {
Text("Initial Value (Optional)")
} footer: {
Text("You can add snapshots later if you prefer.")
}
// Notification Settings
Section {
Picker("Reminder Frequency", selection: $notificationFrequency) {
ForEach(NotificationFrequency.allCases) { frequency in
Text(frequency.displayName).tag(frequency)
}
}
if notificationFrequency == .custom {
Stepper(
"Every \(customFrequencyMonths) month\(customFrequencyMonths > 1 ? "s" : "")",
value: $customFrequencyMonths,
in: 1...24
)
}
} header: {
Text("Reminders")
} footer: {
Text("We'll remind you to update this investment based on your selected frequency.")
}
}
.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
}
}
}
}
// 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,
notificationFrequency: notificationFrequency,
customFrequencyMonths: customFrequencyMonths
)
// 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: "", 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
}
}
// 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
let source: InvestmentSource
@State private var name: String
@State private var selectedCategory: Category?
@State private var notificationFrequency: NotificationFrequency
@State private var customFrequencyMonths: Int
@State private var showingCategoryPicker = false
@StateObject private var categoryRepository = CategoryRepository()
init(source: InvestmentSource) {
self.source = source
_name = State(initialValue: source.name)
_selectedCategory = State(initialValue: source.category)
_notificationFrequency = State(initialValue: source.frequency)
_customFrequencyMonths = State(initialValue: Int(source.customFrequencyMonths))
}
var body: some View {
NavigationStack {
Form {
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)
}
}
}
Section {
Picker("Reminder Frequency", selection: $notificationFrequency) {
ForEach(NotificationFrequency.allCases) { frequency in
Text(frequency.displayName).tag(frequency)
}
}
if notificationFrequency == .custom {
Stepper(
"Every \(customFrequencyMonths) month\(customFrequencyMonths > 1 ? "s" : "")",
value: $customFrequencyMonths,
in: 1...24
)
}
} header: {
Text("Reminders")
}
}
.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,
notificationFrequency: notificationFrequency,
customFrequencyMonths: customFrequencyMonths
)
// Reschedule notification
NotificationService.shared.scheduleReminder(for: source)
dismiss()
}
}
// MARK: - Add Snapshot View
struct AddSnapshotView: View {
@Environment(\.dismiss) private var dismiss
let source: InvestmentSource
@StateObject private var viewModel: SnapshotFormViewModel
init(source: InvestmentSource) {
self.source = source
_viewModel = StateObject(wrappedValue: SnapshotFormViewModel(source: source))
}
var body: some View {
NavigationStack {
Form {
Section {
DatePicker(
"Date",
selection: $viewModel.date,
in: ...Date(),
displayedComponents: .date
)
HStack {
Text("")
.foregroundColor(.secondary)
TextField("Value", text: $viewModel.valueString)
.keyboardType(.decimalPad)
}
if let previous = viewModel.previousValueString {
Text(previous)
.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
)
}
}
}
Section {
Toggle("Include Contribution", isOn: $viewModel.includeContribution)
if viewModel.includeContribution {
HStack {
Text("")
.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()
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())
}