import SwiftUI struct SavingsView: View { @EnvironmentObject var authManager: AuthManager @State private var categories: [SavingsCategory] = [] @State private var stats: SavingsStats? @State private var isLoading = true var body: some View { ZStack { Color(hex: "0a0a1a").ignoresSafeArea() VStack(spacing: 0) { HStack { Text("Накопления").font(.title.bold()).foregroundColor(.white) Spacer() }.padding() if isLoading { ProgressView().tint(Color(hex: "00d4aa")).padding(.top, 40) Spacer() } else { ScrollView { VStack(spacing: 16) { if let s = stats { SavingsTotalCard(stats: s) } VStack(spacing: 10) { ForEach(categories) { cat in SavingsCategoryCard(category: cat) } } .padding(.horizontal) } } } } } .task { await loadData() } .refreshable { await loadData(refresh: true) } } func loadData(refresh: Bool = false) async { if !refresh { isLoading = true } async let cats = APIService.shared.getSavingsCategories(token: authManager.token) async let st = APIService.shared.getSavingsStats(token: authManager.token) categories = (try? await cats) ?? [] stats = try? await st isLoading = false } } struct SavingsTotalCard: View { let stats: SavingsStats var body: some View { VStack(spacing: 20) { VStack(spacing: 6) { Text("Общий баланс").font(.subheadline).foregroundColor(Color(hex: "8888aa")) Text(formatAmount(stats.totalBalance ?? 0)) .font(.system(size: 36, weight: .bold)) .foregroundColor(.white) } HStack(spacing: 0) { VStack(spacing: 4) { Text("Пополнения").font(.caption).foregroundColor(Color(hex: "8888aa")) Text("+\(formatAmount(stats.totalDeposits ?? 0))").font(.callout.bold()).foregroundColor(Color(hex: "00d4aa")) } Spacer() VStack(spacing: 4) { Text("Снятия").font(.caption).foregroundColor(Color(hex: "8888aa")) Text("-\(formatAmount(stats.totalWithdrawals ?? 0))").font(.callout.bold()).foregroundColor(Color(hex: "ff4757")) } Spacer() VStack(spacing: 4) { Text("Категорий").font(.caption).foregroundColor(Color(hex: "8888aa")) Text("\(stats.categoriesCount ?? 0)").font(.callout.bold()).foregroundColor(.white) } } } .padding(20) .background( LinearGradient(colors: [Color(hex: "1a1a3e"), Color(hex: "12122a")], startPoint: .topLeading, endPoint: .bottomTrailing) ) .cornerRadius(20) .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "00d4aa").opacity(0.3), lineWidth: 1)) .padding(.horizontal) } func formatAmount(_ v: Double) -> String { if v >= 1_000_000 { return String(format: "%.2f млн ₽", v / 1_000_000) } else if v >= 1000 { return String(format: "%.0f ₽", v) } return String(format: "%.0f ₽", v) } } struct SavingsCategoryCard: View { let category: SavingsCategory @State private var appeared = false var body: some View { VStack(spacing: 12) { HStack(spacing: 12) { ZStack { Circle() .fill(Color(hex: category.color).opacity(0.2)) .frame(width: 46, height: 46) Image(systemName: category.icon) .foregroundColor(Color(hex: category.color)) .font(.title3) } VStack(alignment: .leading, spacing: 4) { Text(category.name).font(.callout.bold()).foregroundColor(.white) Text(category.typeLabel).font(.caption).foregroundColor(Color(hex: "8888aa")) } Spacer() VStack(alignment: .trailing, spacing: 2) { Text(formatAmount(category.currentAmount ?? 0)) .font(.callout.bold()) .foregroundColor(Color(hex: category.color)) if let end = category.depositEndDate { Text("до \(formatDate(end))") .font(.caption2) .foregroundColor(Color(hex: "8888aa")) } } } if category.isDeposit == true, let target = category.depositAmount, target > 0 { let progress = min((category.currentAmount ?? 0) / target, 1.0) VStack(spacing: 4) { GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 4).fill(Color.white.opacity(0.08)) RoundedRectangle(cornerRadius: 4) .fill(LinearGradient(colors: [Color(hex: "ffa502"), Color(hex: "ff6b35")], startPoint: .leading, endPoint: .trailing)) .frame(width: geo.size.width * CGFloat(appeared ? progress : 0)) .animation(.easeInOut(duration: 0.8), value: appeared) } } .frame(height: 6) HStack { Text("\(Int(progress * 100))%") .font(.caption2).foregroundColor(Color(hex: "ffa502")) Spacer() Text("цель: \(formatAmount(target))") .font(.caption2).foregroundColor(Color(hex: "8888aa")) } } } } .padding(16) .background( RoundedRectangle(cornerRadius: 16) .fill(Color.white.opacity(0.04)) .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color(hex: category.color).opacity(0.15), lineWidth: 1)) ) .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 20) .onAppear { withAnimation(.easeOut(duration: 0.4)) { appeared = true } } } func formatAmount(_ v: Double) -> String { if v >= 1_000_000 { return String(format: "%.2f млн ₽", v / 1_000_000) } return String(format: "%.0f ₽", v) } func formatDate(_ s: String) -> String { let parts = s.prefix(10).split(separator: "-") guard parts.count == 3 else { return s } return "\(parts[2]).\(parts[1]).\(parts[0])" } }