263 lines
8.2 KiB
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())
|
|
}
|