211 lines
6.2 KiB
Swift
211 lines
6.2 KiB
Swift
import SwiftUI
|
|
|
|
struct CategoryBreakdownCard: View {
|
|
@EnvironmentObject private var iapService: IAPService
|
|
let categories: [CategoryMetrics]
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Text("By Category")
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
|
|
NavigationLink {
|
|
ChartsContainerView(iapService: iapService)
|
|
} label: {
|
|
Text("See All")
|
|
.font(.subheadline)
|
|
.foregroundColor(.appPrimary)
|
|
}
|
|
}
|
|
|
|
ForEach(categories) { category in
|
|
CategoryRowView(category: category)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.systemBackground))
|
|
.cornerRadius(AppConstants.UI.cornerRadius)
|
|
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
|
}
|
|
}
|
|
|
|
struct CategoryRowView: View {
|
|
let category: CategoryMetrics
|
|
private var targetPercentage: Double? {
|
|
AllocationTargetStore.target(for: category.id)
|
|
}
|
|
|
|
private var driftText: String? {
|
|
guard let target = targetPercentage else { return nil }
|
|
let drift = category.percentageOfPortfolio - target
|
|
let prefix = drift >= 0 ? "+" : ""
|
|
return "\(prefix)\(String(format: "%.1f%%", drift))"
|
|
}
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
// Icon
|
|
ZStack {
|
|
Circle()
|
|
.fill((Color(hex: category.colorHex) ?? .gray).opacity(0.2))
|
|
.frame(width: 36, height: 36)
|
|
|
|
Image(systemName: category.icon)
|
|
.font(.system(size: 14))
|
|
.foregroundColor(Color(hex: category.colorHex) ?? .gray)
|
|
}
|
|
|
|
// Name and percentage
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(category.categoryName)
|
|
.font(.subheadline.weight(.medium))
|
|
|
|
Text(category.formattedPercentage)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
if let target = targetPercentage {
|
|
Text("Target \(String(format: "%.0f%%", target)) | Drift \(driftText ?? "")")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Value and return
|
|
VStack(alignment: .trailing, spacing: 2) {
|
|
Text(category.formattedTotalValue)
|
|
.font(.subheadline.weight(.semibold))
|
|
|
|
HStack(spacing: 4) {
|
|
Text("CAGR")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
Text(category.metrics.formattedCAGR)
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundColor(category.metrics.cagr >= 0 ? .positiveGreen : .negativeRed)
|
|
}
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
|
|
// MARK: - Category Progress Bar
|
|
|
|
struct CategoryProgressBar: View {
|
|
let category: CategoryMetrics
|
|
let maxValue: Decimal
|
|
|
|
var progress: Double {
|
|
guard maxValue > 0 else { return 0 }
|
|
return NSDecimalNumber(decimal: category.totalValue / maxValue).doubleValue
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack {
|
|
HStack(spacing: 6) {
|
|
Circle()
|
|
.fill(Color(hex: category.colorHex) ?? .gray)
|
|
.frame(width: 8, height: 8)
|
|
|
|
Text(category.categoryName)
|
|
.font(.caption)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Text(category.formattedPercentage)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
GeometryReader { geometry in
|
|
ZStack(alignment: .leading) {
|
|
Rectangle()
|
|
.fill(Color.gray.opacity(0.1))
|
|
.frame(height: 6)
|
|
.cornerRadius(3)
|
|
|
|
Rectangle()
|
|
.fill(Color(hex: category.colorHex) ?? .gray)
|
|
.frame(width: geometry.size.width * progress, height: 6)
|
|
.cornerRadius(3)
|
|
}
|
|
}
|
|
.frame(height: 6)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Simple Category List
|
|
|
|
struct SimpleCategoryList: View {
|
|
let categories: [CategoryMetrics]
|
|
|
|
var body: some View {
|
|
VStack(spacing: 8) {
|
|
ForEach(categories) { category in
|
|
HStack {
|
|
Circle()
|
|
.fill(Color(hex: category.colorHex) ?? .gray)
|
|
.frame(width: 10, height: 10)
|
|
|
|
Text(category.categoryName)
|
|
.font(.subheadline)
|
|
|
|
Spacer()
|
|
|
|
Text(category.formattedTotalValue)
|
|
.font(.subheadline.weight(.medium))
|
|
|
|
Text("(\(category.formattedPercentage))")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
let sampleCategories = [
|
|
CategoryMetrics(
|
|
id: UUID(),
|
|
categoryName: "Stocks",
|
|
colorHex: "#10B981",
|
|
icon: "chart.line.uptrend.xyaxis",
|
|
totalValue: 50000,
|
|
percentageOfPortfolio: 50,
|
|
metrics: .empty
|
|
),
|
|
CategoryMetrics(
|
|
id: UUID(),
|
|
categoryName: "Bonds",
|
|
colorHex: "#3B82F6",
|
|
icon: "building.columns.fill",
|
|
totalValue: 30000,
|
|
percentageOfPortfolio: 30,
|
|
metrics: .empty
|
|
),
|
|
CategoryMetrics(
|
|
id: UUID(),
|
|
categoryName: "Real Estate",
|
|
colorHex: "#F59E0B",
|
|
icon: "house.fill",
|
|
totalValue: 20000,
|
|
percentageOfPortfolio: 20,
|
|
metrics: .empty
|
|
)
|
|
]
|
|
|
|
return CategoryBreakdownCard(categories: sampleCategories)
|
|
.padding()
|
|
.environmentObject(IAPService())
|
|
}
|