import SwiftUI import Charts struct SourceDetailView: View { @StateObject private var viewModel: SourceDetailViewModel @Environment(\.dismiss) private var dismiss @AppStorage("calmModeEnabled") private var calmModeEnabled = true @State private var showingDeleteConfirmation = false @State private var editingSnapshot: Snapshot? init(source: InvestmentSource, iapService: IAPService) { _viewModel = StateObject(wrappedValue: SourceDetailViewModel( source: source, iapService: iapService )) } var body: some View { ZStack { AppBackground() ScrollView { VStack(spacing: 20) { // Header Card headerCard // Quick Actions quickActions // Chart if !viewModel.chartData.isEmpty { chartSection } // Metrics if calmModeEnabled { simpleMetricsSection } else { metricsSection } // Transactions transactionsSection // Snapshots List snapshotsSection } .padding() } } .navigationTitle(viewModel.source.name) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Menu { Button { viewModel.showingEditSource = true } label: { Label("Edit Source", systemImage: "pencil") } Button(role: .destructive) { showingDeleteConfirmation = true } label: { Label("Delete Source", systemImage: "trash") } } label: { Image(systemName: "ellipsis.circle") } } } .sheet(isPresented: $viewModel.showingAddSnapshot) { AddSnapshotView(source: viewModel.source) } .sheet(item: $editingSnapshot) { snapshot in AddSnapshotView(source: viewModel.source, snapshot: snapshot) } .sheet(isPresented: $viewModel.showingAddTransaction) { AddTransactionView { type, date, shares, price, amount, notes in viewModel.addTransaction( type: type, date: date, shares: shares, price: price, amount: amount, notes: notes ) } } .sheet(isPresented: $viewModel.showingEditSource) { EditSourceView(source: viewModel.source) } .sheet(isPresented: $viewModel.showingPaywall) { PaywallView() } .confirmationDialog( "Delete Source", isPresented: $showingDeleteConfirmation, titleVisibility: .visible ) { Button("Delete", role: .destructive) { // Delete and dismiss let repository = InvestmentSourceRepository() repository.deleteSource(viewModel.source) dismiss() } } message: { Text("This will permanently delete \(viewModel.source.name) and all its snapshots.") } } // MARK: - Header Card private var headerCard: some View { VStack(spacing: 12) { // Category badge HStack { Image(systemName: viewModel.source.category?.icon ?? "questionmark") Text(viewModel.categoryName) } .font(.caption) .foregroundColor(Color(hex: viewModel.categoryColor) ?? .gray) .padding(.horizontal, 12) .padding(.vertical, 6) .background((Color(hex: viewModel.categoryColor) ?? .gray).opacity(0.1)) .cornerRadius(20) // Current value Text(viewModel.formattedCurrentValue) .font(.system(size: 36, weight: .bold, design: .rounded)) // Return if calmModeEnabled { VStack(spacing: 2) { Text("All-time return") .font(.caption) .foregroundColor(.secondary) Text("\(viewModel.formattedTotalReturn) (\(viewModel.formattedPercentageReturn))") .font(.subheadline.weight(.medium)) .foregroundColor(.secondary) } } else { HStack(spacing: 4) { Image(systemName: viewModel.isPositiveReturn ? "arrow.up.right" : "arrow.down.right") Text(viewModel.formattedTotalReturn) Text("(\(viewModel.formattedPercentageReturn))") } .font(.subheadline.weight(.medium)) .foregroundColor(viewModel.isPositiveReturn ? .positiveGreen : .negativeRed) } // Last updated Text("Last updated: \(viewModel.lastUpdated)") .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity) .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } // MARK: - Quick Actions private var quickActions: some View { HStack(spacing: 12) { Button { viewModel.showingAddSnapshot = true } label: { Label("Add Snapshot", systemImage: "plus.circle.fill") .font(.subheadline.weight(.semibold)) .frame(maxWidth: .infinity) .padding() .background(Color.appPrimary) .foregroundColor(.white) .cornerRadius(AppConstants.UI.cornerRadius) } Button { viewModel.showingAddTransaction = true } label: { Label("Add Transaction", systemImage: "arrow.left.arrow.right.circle") .font(.subheadline.weight(.semibold)) .frame(maxWidth: .infinity) .padding() .background(Color.appSecondary.opacity(0.15)) .foregroundColor(.appSecondary) .cornerRadius(AppConstants.UI.cornerRadius) } } } // MARK: - Chart Section private var chartSection: some View { VStack(alignment: .leading, spacing: 12) { Text("Value History") .font(.headline) Chart { ForEach(viewModel.chartData, id: \.date) { item in LineMark( x: .value("Date", item.date), y: .value("Value", NSDecimalNumber(decimal: item.value).doubleValue) ) .foregroundStyle(Color.appPrimary) .interpolationMethod(.catmullRom) AreaMark( x: .value("Date", item.date), y: .value("Value", NSDecimalNumber(decimal: item.value).doubleValue) ) .foregroundStyle( LinearGradient( colors: [Color.appPrimary.opacity(0.3), Color.appPrimary.opacity(0.0)], startPoint: .top, endPoint: .bottom ) ) .interpolationMethod(.catmullRom) } if viewModel.canViewPredictions, !viewModel.predictions.isEmpty { ForEach(viewModel.predictions, id: \.id) { prediction in AreaMark( x: .value("Prediction Date", prediction.date), yStart: .value("Lower", NSDecimalNumber(decimal: prediction.confidenceInterval.lower).doubleValue), yEnd: .value("Upper", NSDecimalNumber(decimal: prediction.confidenceInterval.upper).doubleValue) ) .foregroundStyle(Color.appSecondary.opacity(0.18)) LineMark( x: .value("Prediction Date", prediction.date), y: .value("Predicted Value", NSDecimalNumber(decimal: prediction.predictedValue).doubleValue) ) .foregroundStyle(Color.appSecondary) .lineStyle(StrokeStyle(lineWidth: 2, dash: [6, 4])) } if let firstPrediction = viewModel.predictions.first { PointMark( x: .value("Forecast Start", firstPrediction.date), y: .value("Forecast Value", NSDecimalNumber(decimal: firstPrediction.predictedValue).doubleValue) ) .symbolSize(30) .foregroundStyle(Color.appSecondary) .annotation(position: .topTrailing) { Text("Forecast") .font(.caption.weight(.semibold)) .padding(.horizontal, 6) .padding(.vertical, 4) .background(Color.appSecondary.opacity(0.15)) .cornerRadius(8) } } } } .chartXAxis { AxisMarks(values: .stride(by: .month, count: 2)) { value in AxisValueLabel(format: .dateTime.month(.abbreviated)) } } .chartYAxis { AxisMarks(position: .leading) { value in AxisValueLabel { if let doubleValue = value.as(Double.self) { Text(Decimal(doubleValue).shortCurrencyString) .font(.caption) } } } } .frame(height: 200) if !viewModel.canViewPredictions && viewModel.hasEnoughDataForPredictions { Button { viewModel.showingPaywall = true } label: { HStack(spacing: 6) { Image(systemName: "lock.fill") Text("Unlock predictions on the chart") } .font(.caption.weight(.semibold)) .foregroundColor(.appPrimary) .padding(.vertical, 6) .padding(.horizontal, 10) .background(Color.appPrimary.opacity(0.1)) .cornerRadius(10) } .frame(maxWidth: .infinity, alignment: .leading) } } .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } // MARK: - Metrics Section private var metricsSection: some View { VStack(alignment: .leading, spacing: 12) { Text("Performance Metrics") .font(.headline) LazyVGrid(columns: [ GridItem(.flexible()), GridItem(.flexible()) ], spacing: 16) { MetricCard(title: "CAGR", value: viewModel.metrics.formattedCAGR) MetricCard(title: "Volatility", value: viewModel.metrics.formattedVolatility) MetricCard(title: "Max Drawdown", value: viewModel.metrics.formattedMaxDrawdown) MetricCard(title: "Sharpe Ratio", value: viewModel.metrics.formattedSharpeRatio) MetricCard(title: "Win Rate", value: viewModel.metrics.formattedWinRate) MetricCard(title: "Avg Monthly", value: viewModel.metrics.formattedAverageMonthlyReturn) } } .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } private var simpleMetricsSection: some View { VStack(alignment: .leading, spacing: 12) { Text("Performance Summary") .font(.headline) HStack(spacing: 12) { MetricChip(title: "CAGR", value: viewModel.metrics.formattedCAGR) MetricChip(title: "Avg Monthly", value: viewModel.metrics.formattedAverageMonthlyReturn) MetricChip(title: "Contributions", value: viewModel.source.totalContributions.compactCurrencyString) } } .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } // MARK: - Transactions Section private var transactionsSection: some View { VStack(alignment: .leading, spacing: 12) { HStack { Text("Transactions") .font(.headline) Spacer() Text("\(viewModel.transactions.count)") .font(.subheadline) .foregroundColor(.secondary) } if viewModel.transactions.isEmpty { Text("Add buys, sells, dividends, and fees to track cashflows.") .font(.subheadline) .foregroundColor(.secondary) } else { HStack(spacing: 12) { MetricChip(title: "Invested", value: viewModel.source.totalInvested.compactCurrencyString) MetricChip(title: "Dividends", value: viewModel.source.totalDividends.compactCurrencyString) MetricChip(title: "Fees", value: viewModel.source.totalFees.compactCurrencyString) } ForEach(viewModel.transactions.prefix(5)) { transaction in TransactionRow(transaction: transaction) .contextMenu { Button(role: .destructive) { viewModel.deleteTransaction(transaction) } label: { Label("Delete", systemImage: "trash") } } } if viewModel.transactions.count > 5 { Text("+ \(viewModel.transactions.count - 5) more") .font(.caption) .foregroundColor(.secondary) } } } .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } // MARK: - Snapshots Section private var snapshotsSection: some View { VStack(alignment: .leading, spacing: 12) { HStack { Text("Snapshots") .font(.headline) Spacer() Text("\(viewModel.snapshotCount)") .font(.subheadline) .foregroundColor(.secondary) } if viewModel.isHistoryLimited { HStack { Image(systemName: "info.circle") .foregroundColor(.appWarning) Text("\(viewModel.hiddenSnapshotCount) older snapshots hidden. Upgrade for full history.") .font(.caption) Spacer() Button("Upgrade") { viewModel.showingPaywall = true } .font(.caption.weight(.semibold)) } .padding(8) .background(Color.appWarning.opacity(0.1)) .cornerRadius(AppConstants.UI.smallCornerRadius) } ForEach(viewModel.visibleSnapshots) { snapshot in SnapshotRowView(snapshot: snapshot, onEdit: { editingSnapshot = snapshot }) .contentShape(Rectangle()) .onTapGesture { editingSnapshot = snapshot } .contextMenu { Button { editingSnapshot = snapshot } label: { Label("Edit", systemImage: "pencil") } } .swipeActions(edge: .trailing) { Button { editingSnapshot = snapshot } label: { Label("Edit", systemImage: "pencil") } Button(role: .destructive) { viewModel.deleteSnapshot(snapshot) } label: { Label("Delete", systemImage: "trash") } } if snapshot.id != viewModel.visibleSnapshots.last?.id { Divider() } } if viewModel.snapshots.count > 10 { Text("+ \(viewModel.snapshots.count - 10) more snapshots") .font(.caption) .foregroundColor(.secondary) } } .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } } // MARK: - Metric Card struct MetricCard: View { let title: String let value: String var body: some View { VStack(alignment: .leading, spacing: 4) { Text(title) .font(.caption) .foregroundColor(.secondary) Text(value) .font(.subheadline.weight(.semibold)) } .frame(maxWidth: .infinity, alignment: .leading) } } // MARK: - Snapshot Row View struct SnapshotRowView: View { let snapshot: Snapshot let onEdit: (() -> Void)? var body: some View { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 2) { Text(snapshot.date.friendlyDescription) .font(.subheadline) if let notes = snapshot.notes, !notes.isEmpty { Text(notes) .font(.caption) .foregroundColor(.secondary) .lineLimit(1) } } Spacer() VStack(alignment: .trailing, spacing: 2) { Text(snapshot.formattedValue) .font(.subheadline.weight(.medium)) if snapshot.contribution != nil && snapshot.decimalContribution > 0 { Text("+ \(snapshot.decimalContribution.currencyString)") .font(.caption) .foregroundColor(.appPrimary) } } if let onEdit = onEdit { Button(action: onEdit) { Image(systemName: "pencil") .font(.caption) .foregroundColor(.secondary) .padding(6) .background(Color.gray.opacity(0.1)) .cornerRadius(8) } .buttonStyle(.plain) } } .padding(.vertical, 4) } } // MARK: - Transaction Row View struct TransactionRow: View { let transaction: Transaction var body: some View { HStack { VStack(alignment: .leading, spacing: 2) { Text(transaction.transactionType.displayName) .font(.subheadline.weight(.semibold)) Text(transaction.date.friendlyDescription) .font(.caption) .foregroundColor(.secondary) } Spacer() Text(transaction.decimalAmount.compactCurrencyString) .font(.subheadline.weight(.semibold)) .foregroundColor(transaction.transactionType == .fee ? .negativeRed : .primary) } .padding(.vertical, 4) } } struct MetricChip: View { let title: String let value: String var body: some View { VStack(alignment: .leading, spacing: 4) { Text(title) .font(.caption) .foregroundColor(.secondary) Text(value) .font(.caption.weight(.semibold)) } .padding(.vertical, 8) .padding(.horizontal, 10) .background(Color.gray.opacity(0.1)) .cornerRadius(10) } } #Preview { NavigationStack { Text("Source Detail Preview") } }