InvestmentTrackerApp/InvestmentTracker/Views/Dashboard/DashboardView.swift

263 lines
8.2 KiB
Swift

import SwiftUI
import Charts
struct DashboardView: View {
@EnvironmentObject var iapService: IAPService
@StateObject private var viewModel: DashboardViewModel
init() {
_viewModel = StateObject(wrappedValue: DashboardViewModel())
}
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 20) {
if viewModel.hasData {
// Total Value Card
TotalValueCard(
totalValue: viewModel.portfolioSummary.formattedTotalValue,
dayChange: viewModel.portfolioSummary.formattedDayChange,
isPositive: viewModel.isDayChangePositive
)
// Evolution Chart
if !viewModel.evolutionData.isEmpty {
EvolutionChartCard(data: viewModel.evolutionData)
}
// Period Returns
PeriodReturnsCard(
monthChange: viewModel.portfolioSummary.formattedMonthChange,
yearChange: viewModel.portfolioSummary.formattedYearChange,
allTimeChange: viewModel.portfolioSummary.formattedAllTimeReturn,
isMonthPositive: viewModel.isMonthChangePositive,
isYearPositive: viewModel.isYearChangePositive,
isAllTimePositive: viewModel.portfolioSummary.allTimeReturn >= 0
)
// Category Breakdown
if !viewModel.categoryMetrics.isEmpty {
CategoryBreakdownCard(categories: viewModel.topCategories)
}
// Pending Updates
if !viewModel.sourcesNeedingUpdate.isEmpty {
PendingUpdatesCard(sources: viewModel.sourcesNeedingUpdate)
}
} else {
EmptyDashboardView()
}
}
.padding()
}
.navigationTitle("Dashboard")
.refreshable {
viewModel.refreshData()
}
.overlay {
if viewModel.isLoading {
ProgressView()
}
}
}
}
}
// MARK: - Total Value Card
struct TotalValueCard: View {
let totalValue: String
let dayChange: String
let isPositive: Bool
var body: some View {
VStack(spacing: 8) {
Text("Total Portfolio Value")
.font(.subheadline)
.foregroundColor(.secondary)
Text(totalValue)
.font(.system(size: 42, weight: .bold, design: .rounded))
.foregroundColor(.primary)
HStack(spacing: 4) {
Image(systemName: isPositive ? "arrow.up.right" : "arrow.down.right")
.font(.caption)
Text(dayChange)
.font(.subheadline.weight(.medium))
Text("today")
.font(.subheadline)
.foregroundColor(.secondary)
}
.foregroundColor(isPositive ? .positiveGreen : .negativeRed)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
}
// MARK: - Period Returns Card
struct PeriodReturnsCard: View {
let monthChange: String
let yearChange: String
let allTimeChange: String
let isMonthPositive: Bool
let isYearPositive: Bool
let isAllTimePositive: Bool
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Returns")
.font(.headline)
HStack(spacing: 16) {
ReturnPeriodView(
period: "1M",
change: monthChange,
isPositive: isMonthPositive
)
Divider()
ReturnPeriodView(
period: "1Y",
change: yearChange,
isPositive: isYearPositive
)
Divider()
ReturnPeriodView(
period: "All",
change: allTimeChange,
isPositive: isAllTimePositive
)
}
.frame(maxWidth: .infinity)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
}
struct ReturnPeriodView: View {
let period: String
let change: String
let isPositive: Bool
var body: some View {
VStack(spacing: 4) {
Text(period)
.font(.caption)
.foregroundColor(.secondary)
Text(change)
.font(.subheadline.weight(.semibold))
.foregroundColor(isPositive ? .positiveGreen : .negativeRed)
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.frame(maxWidth: .infinity)
}
}
// MARK: - Empty Dashboard View
struct EmptyDashboardView: View {
var body: some View {
VStack(spacing: 20) {
Image(systemName: "chart.pie")
.font(.system(size: 60))
.foregroundColor(.secondary)
Text("Welcome to Investment Tracker")
.font(.title2.weight(.semibold))
Text("Start by adding your first investment source to track your portfolio.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
NavigationLink {
AddSourceView()
} label: {
Label("Add Investment Source", systemImage: "plus")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.appPrimary)
.cornerRadius(AppConstants.UI.cornerRadius)
}
}
.padding()
}
}
// MARK: - Pending Updates Card
struct PendingUpdatesCard: View {
let sources: [InvestmentSource]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: "bell.badge.fill")
.foregroundColor(.appWarning)
Text("Pending Updates")
.font(.headline)
Spacer()
Text("\(sources.count)")
.font(.subheadline)
.foregroundColor(.secondary)
}
ForEach(sources.prefix(3)) { source in
NavigationLink(destination: SourceDetailView(source: source)) {
HStack {
Circle()
.fill(source.category?.color ?? .gray)
.frame(width: 8, height: 8)
Text(source.name)
.font(.subheadline)
Spacer()
Text(source.latestSnapshot?.date.relativeDescription ?? "Never")
.font(.caption)
.foregroundColor(.secondary)
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
}
.buttonStyle(.plain)
}
if sources.count > 3 {
Text("+ \(sources.count - 3) more")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
}
#Preview {
DashboardView()
.environmentObject(IAPService())
}