InvestmentTrackerApp/InvestmentTracker/Views/Sources/SourceDetailView.swift

427 lines
14 KiB
Swift

import SwiftUI
import Charts
struct SourceDetailView: View {
@EnvironmentObject var iapService: IAPService
@StateObject private var viewModel: SourceDetailViewModel
@Environment(\.dismiss) private var dismiss
@State private var showingDeleteConfirmation = false
init(source: InvestmentSource) {
_viewModel = StateObject(wrappedValue: SourceDetailViewModel(
source: source,
iapService: IAPService()
))
}
var body: some View {
ScrollView {
VStack(spacing: 20) {
// Header Card
headerCard
// Quick Actions
quickActions
// Chart
if !viewModel.chartData.isEmpty {
chartSection
}
// Metrics
metricsSection
// Predictions (Premium)
if viewModel.hasEnoughDataForPredictions {
predictionsSection
}
// 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(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
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)
}
}
}
// MARK: - Chart Section
private var chartSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Value History")
.font(.headline)
Chart(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)
}
.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)
}
.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)
}
// MARK: - Predictions Section
private var predictionsSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Predictions")
.font(.headline)
Spacer()
if !viewModel.canViewPredictions {
Label("Premium", systemImage: "crown.fill")
.font(.caption)
.foregroundColor(.appWarning)
}
}
if viewModel.canViewPredictions {
if let result = viewModel.predictionResult, !viewModel.predictions.isEmpty {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Algorithm: \(result.algorithm.displayName)")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text("Confidence: \(result.confidenceLevel.rawValue)")
.font(.caption)
.foregroundColor(Color(hex: result.confidenceLevel.color) ?? .gray)
}
// Show 12-month prediction
if let lastPrediction = viewModel.predictions.last {
HStack {
VStack(alignment: .leading) {
Text("12-Month Forecast")
.font(.subheadline)
.foregroundColor(.secondary)
Text(lastPrediction.formattedValue)
.font(.title3.weight(.semibold))
}
Spacer()
VStack(alignment: .trailing) {
Text("Range")
.font(.subheadline)
.foregroundColor(.secondary)
Text(lastPrediction.formattedConfidenceRange)
.font(.caption)
}
}
}
}
}
} else {
Button {
viewModel.showingPaywall = true
} label: {
HStack {
Image(systemName: "lock.fill")
Text("Unlock Predictions with Premium")
}
.font(.subheadline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.appPrimary.opacity(0.1))
.foregroundColor(.appPrimary)
.cornerRadius(AppConstants.UI.smallCornerRadius)
}
}
}
.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.snapshots.prefix(10)) { snapshot in
SnapshotRowView(snapshot: snapshot)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
viewModel.deleteSnapshot(snapshot)
} label: {
Label("Delete", systemImage: "trash")
}
}
if snapshot.id != viewModel.snapshots.prefix(10).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
var body: some View {
HStack {
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)
}
}
}
.padding(.vertical, 4)
}
}
#Preview {
NavigationStack {
Text("Source Detail Preview")
}
}