import SwiftUI struct SourceListView: View { @EnvironmentObject var iapService: IAPService @StateObject private var viewModel: SourceListViewModel init() { // We'll set the iapService in onAppear since we need @EnvironmentObject _viewModel = StateObject(wrappedValue: SourceListViewModel(iapService: IAPService())) } var body: some View { NavigationStack { Group { if viewModel.isEmpty { emptyStateView } else { sourcesList } } .navigationTitle("Sources") .searchable(text: $viewModel.searchText, prompt: "Search sources") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { viewModel.addSourceTapped() } label: { Image(systemName: "plus") } } ToolbarItem(placement: .navigationBarLeading) { categoryFilterMenu } } .sheet(isPresented: $viewModel.showingAddSource) { AddSourceView() } .sheet(isPresented: $viewModel.showingPaywall) { PaywallView() } } } // MARK: - Sources List private var sourcesList: some View { List { // Summary Header if !viewModel.isFiltered { Section { HStack { VStack(alignment: .leading) { Text("Total Value") .font(.caption) .foregroundColor(.secondary) Text(viewModel.formattedTotalValue) .font(.title2.weight(.bold)) } Spacer() VStack(alignment: .trailing) { Text("Sources") .font(.caption) .foregroundColor(.secondary) Text("\(viewModel.sources.count)") .font(.title2.weight(.bold)) } } .padding(.vertical, 4) } } // Source limit warning if viewModel.sourceLimitReached { Section { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.appWarning) Text("Source limit reached. Upgrade to Premium for unlimited sources.") .font(.subheadline) Spacer() Button("Upgrade") { viewModel.showingPaywall = true } .font(.subheadline.weight(.semibold)) .foregroundColor(.appPrimary) } } } // Sources Section { ForEach(viewModel.sources) { source in NavigationLink { SourceDetailView(source: source) } label: { SourceRowView(source: source) } } .onDelete { indexSet in viewModel.deleteSource(at: indexSet) } } header: { if viewModel.isFiltered { HStack { Text("\(viewModel.sources.count) results") Spacer() Button("Clear Filters") { viewModel.clearFilters() } .font(.caption) } } } } .listStyle(.insetGrouped) .refreshable { viewModel.loadData() } } // MARK: - Empty State private var emptyStateView: some View { VStack(spacing: 20) { Image(systemName: "tray") .font(.system(size: 60)) .foregroundColor(.secondary) Text("No Investment Sources") .font(.title2.weight(.semibold)) Text("Add your first investment source to start tracking your portfolio.") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal, 40) Button { viewModel.showingAddSource = true } label: { Label("Add Source", systemImage: "plus") .font(.headline) .foregroundColor(.white) .padding() .frame(maxWidth: 200) .background(Color.appPrimary) .cornerRadius(AppConstants.UI.cornerRadius) } } } // MARK: - Category Filter Menu private var categoryFilterMenu: some View { Menu { Button { viewModel.selectCategory(nil) } label: { HStack { Text("All Categories") if viewModel.selectedCategory == nil { Image(systemName: "checkmark") } } } Divider() ForEach(viewModel.categories) { category in Button { viewModel.selectCategory(category) } label: { HStack { Image(systemName: category.icon) Text(category.name) if viewModel.selectedCategory?.id == category.id { Image(systemName: "checkmark") } } } } } label: { HStack(spacing: 4) { Image(systemName: "line.3.horizontal.decrease.circle") if viewModel.selectedCategory != nil { Circle() .fill(Color.appPrimary) .frame(width: 8, height: 8) } } } } } // MARK: - Source Row View struct SourceRowView: View { let source: InvestmentSource var body: some View { HStack(spacing: 12) { // Category indicator ZStack { Circle() .fill((source.category?.color ?? .gray).opacity(0.2)) .frame(width: 44, height: 44) Image(systemName: source.category?.icon ?? "questionmark") .font(.system(size: 18)) .foregroundColor(source.category?.color ?? .gray) } // Source info VStack(alignment: .leading, spacing: 4) { Text(source.name) .font(.headline) HStack(spacing: 8) { Text(source.category?.name ?? "Uncategorized") .font(.caption) .foregroundColor(.secondary) if source.needsUpdate { Label("Needs update", systemImage: "bell.badge.fill") .font(.caption2) .foregroundColor(.appWarning) } } } Spacer() // Value VStack(alignment: .trailing, spacing: 4) { Text(source.latestValue.compactCurrencyString) .font(.subheadline.weight(.semibold)) HStack(spacing: 2) { Image(systemName: source.totalReturn >= 0 ? "arrow.up.right" : "arrow.down.right") .font(.caption2) Text(String(format: "%.1f%%", NSDecimalNumber(decimal: source.totalReturn).doubleValue)) .font(.caption) } .foregroundColor(source.totalReturn >= 0 ? .positiveGreen : .negativeRed) } } .padding(.vertical, 4) .opacity(source.isActive ? 1 : 0.5) } } #Preview { SourceListView() .environmentObject(IAPService()) }