initial version
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user