import SwiftUI struct SavingsView: View { @EnvironmentObject var authManager: AuthManager @State private var selectedTab = 0 var body: some View { ZStack { Color(hex: "0a0a1a").ignoresSafeArea() VStack(spacing: 0) { // Header HStack { Text("Накопления").font(.title.bold()).foregroundColor(.white) Spacer() }.padding() // Segment control Picker("", selection: $selectedTab) { Text("Обзор").tag(0) Text("Транзакции").tag(1) } .pickerStyle(.segmented) .padding(.horizontal) .padding(.bottom, 12) if selectedTab == 0 { SavingsOverviewTab() } else { SavingsTransactionsTab() } } } } } struct SavingsOverviewTab: View { @EnvironmentObject var authManager: AuthManager @State private var categories: [SavingsCategory] = [] @State private var stats: SavingsStats? @State private var isLoading = true var body: some View { Group { 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 SavingsTransactionsTab: View { @EnvironmentObject var authManager: AuthManager @State private var transactions: [SavingsTransaction] = [] @State private var isLoading = true var body: some View { Group { if isLoading { ProgressView().tint(Color(hex: "00d4aa")).padding(.top, 40) Spacer() } else if transactions.isEmpty { VStack(spacing: 12) { Text("💸").font(.system(size: 50)) Text("Нет транзакций").foregroundColor(Color(hex: "8888aa")) }.padding(.top, 60) Spacer() } else { List { ForEach(transactions) { tx in SavingsTransactionRow(transaction: tx) .listRowBackground(Color.clear) .listRowSeparator(.hidden) } } .listStyle(.plain) .scrollContentBackground(.hidden) } } .task { await loadData() } .refreshable { await loadData(refresh: true) } } func loadData(refresh: Bool = false) async { if !refresh { isLoading = true } transactions = (try? await APIService.shared.getSavingsTransactions(token: authManager.token)) ?? [] isLoading = false } } struct SavingsTransactionRow: View { let transaction: SavingsTransaction var body: some View { HStack(spacing: 12) { ZStack { Circle() .fill((transaction.isDeposit ? Color(hex: "00d4aa") : Color(hex: "ff4757")).opacity(0.15)) .frame(width: 40, height: 40) Image(systemName: transaction.isDeposit ? "arrow.down.circle.fill" : "arrow.up.circle.fill") .foregroundColor(transaction.isDeposit ? Color(hex: "00d4aa") : Color(hex: "ff4757")) } VStack(alignment: .leading, spacing: 3) { Text(transaction.categoryName ?? "Без категории") .font(.callout).foregroundColor(.white) HStack(spacing: 6) { if let userName = transaction.userName { Text(userName).font(.caption).foregroundColor(Color(hex: "8888aa")) } if let date = transaction.date { Text(formatDate(date)).font(.caption2).foregroundColor(Color(hex: "8888aa")) } } } Spacer() Text("\(transaction.isDeposit ? "+" : "-")\(formatAmount(transaction.amount))") .font(.callout.bold()) .foregroundColor(transaction.isDeposit ? Color(hex: "00d4aa") : Color(hex: "ff4757")) } .padding(12) .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.04))) .padding(.horizontal) .padding(.vertical, 2) } func formatAmount(_ v: Double) -> String { if v >= 1_000_000 { return String(format: "%.1f млн ₽", 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 String(s.prefix(10)) } return "\(parts[2]).\(parts[1]).\(parts[0])" } } // MARK: - SavingsTotalCard 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) } } // MARK: - SavingsCategoryCard 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])" } }