InvestmentTrackerApp/InvestmentTracker/Views/Sources/SourceListView.swift

262 lines
8.3 KiB
Swift

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())
}