diff --git a/Coinly/Features/Dashboard/DashboardView.swift b/Coinly/Features/Dashboard/DashboardView.swift index 2ed650e..735e3e9 100644 --- a/Coinly/Features/Dashboard/DashboardView.swift +++ b/Coinly/Features/Dashboard/DashboardView.swift @@ -1,15 +1,58 @@ import SwiftUI +struct BudgetProgress: View { + let spent: Double + let total: Double + let color: Color + + private var percentage: Double { + guard total > 0 else { return 0 } + return min(spent / total, 1.0) + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + GeometryReader { geometry in + ZStack(alignment: .leading) { + Rectangle() + .fill(Color(uiColor: .systemFill)) + .frame(height: 6) + .cornerRadius(3) + + Rectangle() + .fill(color) + .frame(width: geometry.size.width * CGFloat(percentage), height: 6) + .cornerRadius(3) + } + } + .frame(height: 6) + + HStack { + Text("\(Int(percentage * 100))% spent") + .font(.caption) + .foregroundColor(.secondary) + + Spacer() + + Text("\(spent.formatAsCurrency()) of \(total.formatAsCurrency())") + .font(.caption) + .foregroundColor(.secondary) + } + } + } +} + struct DashboardView: View { @ObservedObject var store: TransactionsStore @ObservedObject var accountsStore: AccountsStore @EnvironmentObject private var settings: AppSettings @State private var showingAddAccount = false @State private var selectedAccount: AccountModel? + @AppStorage("includeCreditCards") private var includeCreditCards = false - private var totalAccountsBalance: Double { + private var adjustedBalance: Double { accountsStore.accounts.reduce(0) { total, account in - total + (account.type == .creditCard ? 0 : account.balance) + total + ((!includeCreditCards && account.type == .creditCard) ? 0 : account.balance) } } @@ -32,16 +75,43 @@ struct DashboardView: View { .reduce(0) { $0 + $1.amountInCurrentCurrency() } } + private var categoryExpenses: [(category: CategoryModel, amount: Double, percentage: Double)] { + let expensesByCategory = Dictionary(grouping: thisMonthTransactions.filter { $0.isExpense }) { + $0.category + } + + let totalExpenses = monthlyExpenses + + return expensesByCategory.map { (category, transactions) in + let categoryModel = CategoryModel.category(for: category) + let amount = transactions.reduce(0) { $0 + $1.amountInCurrentCurrency() } + let percentage = totalExpenses > 0 ? amount / totalExpenses : 0 + return (categoryModel, amount, percentage) + } + .sorted { $0.amount > $1.amount } + } + var body: some View { ScrollView { VStack(spacing: 24) { // Monthly Summary VStack(spacing: 8) { - Text("This Month") - .font(.subheadline) - .foregroundColor(Color(uiColor: .secondaryLabel)) + HStack { + Text("This Month") + .font(.subheadline) + .foregroundColor(Color(uiColor: .secondaryLabel)) + + Spacer() + + Menu { + Toggle("Include Credit Cards", isOn: $includeCreditCards) + } label: { + Image(systemName: "ellipsis.circle") + .foregroundColor(.secondary) + } + } - Text(totalAccountsBalance.formatAsCurrency()) + Text(adjustedBalance.formatAsCurrency()) .font(.system(size: 34, weight: .bold)) HStack(spacing: 20) { @@ -97,11 +167,76 @@ struct DashboardView: View { .padding(.horizontal) // Spending Analysis Section - if !monthlyExpenses.isZero { + if !categoryExpenses.isEmpty { VStack(alignment: .leading, spacing: 16) { - Text("Spending Analysis") - .font(.title2) - .fontWeight(.bold) + HStack { + Text("Spending Analysis") + .font(.title2) + .fontWeight(.bold) + + Spacer() + + Text("Monthly Budget") + .font(.subheadline) + .foregroundColor(.secondary) + } + + BudgetProgress( + spent: monthlyExpenses, + total: 2000, // В будущем можно добавить настраиваемый бюджет + color: monthlyExpenses > 2000 ? Color(uiColor: .systemRed) : Color(uiColor: .systemBlue) + ) + .padding(.bottom) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(categoryExpenses, id: \.category.id) { item in + VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 12) { + Image(systemName: item.category.icon) + .font(.title2) + .foregroundColor(.white) + .frame(width: 44, height: 44) + .background(Color(item.category.color)) + .cornerRadius(12) + + VStack(alignment: .leading, spacing: 4) { + Text(item.category.name) + .font(.headline) + Text(item.amount.formatAsCurrency()) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + + // Progress Bar + VStack(alignment: .leading, spacing: 8) { + GeometryReader { geometry in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 4) + .fill(Color(uiColor: .systemFill)) + .frame(height: 8) + + RoundedRectangle(cornerRadius: 4) + .fill(Color(item.category.color)) + .frame(width: geometry.size.width * CGFloat(item.percentage), height: 8) + } + } + .frame(height: 8) + + Text(String(format: "%.1f%%", item.percentage * 100)) + .font(.caption) + .foregroundColor(.secondary) + } + } + .frame(width: 200) + .padding() + .background(Color(uiColor: .secondarySystemBackground)) + .cornerRadius(16) + } + } + .padding(.horizontal) + } } .padding(.horizontal) } diff --git a/Coinly/Features/Transactions/TransactionsView.swift b/Coinly/Features/Transactions/TransactionsView.swift index db10dc5..96484df 100644 --- a/Coinly/Features/Transactions/TransactionsView.swift +++ b/Coinly/Features/Transactions/TransactionsView.swift @@ -6,19 +6,13 @@ struct TransactionsView: View { @State private var showingFilters = false @State private var searchText = "" @State private var filter = TransactionFilter() - @State private var sortOrder = TransactionsStore.SortOrder.dateDescending - @State private var transactionToEdit: TransactionModel? - - private var filteredAndSortedTransactions: [TransactionModel] { - let filtered = store.filterTransactions(searchText: searchText, filter: filter) - return store.sortTransactions(filtered, by: sortOrder) - } private var groupedTransactions: [(String, [TransactionModel])] { let formatter = DateFormatter() formatter.dateFormat = "MMMM yyyy" - let grouped = Dictionary(grouping: filteredAndSortedTransactions) { transaction in + let filtered = store.filterTransactions(searchText: searchText, filter: filter) + let grouped = Dictionary(grouping: filtered) { transaction in formatter.string(from: transaction.date) } @@ -28,25 +22,22 @@ struct TransactionsView: View { var body: some View { NavigationView { ZStack { - Color(uiColor: .systemBackground) + Color(uiColor: .systemGroupedBackground) .ignoresSafeArea() - if filteredAndSortedTransactions.isEmpty { + if groupedTransactions.isEmpty { EmptyTransactionsView() } else { ScrollView { LazyVStack(spacing: 24) { ForEach(groupedTransactions, id: \.0) { month, transactions in - VStack(spacing: 8) { + VStack(alignment: .leading, spacing: 12) { // Month Header - HStack { - Text(month) - .font(.system(size: 15, weight: .semibold)) - .foregroundColor(Color(uiColor: .secondaryLabel)) - .textCase(.uppercase) - Spacer() - } - .padding(.horizontal) + Text(month) + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(Color(uiColor: .secondaryLabel)) + .textCase(.uppercase) + .padding(.horizontal) // Transactions VStack(spacing: 1) { @@ -54,7 +45,7 @@ struct TransactionsView: View { TransactionRowView(transaction: transaction) .contextMenu { Button { - transactionToEdit = transaction + // Edit transaction } label: { Label("Edit", systemImage: "pencil") } @@ -65,26 +56,15 @@ struct TransactionsView: View { Label("Delete", systemImage: "trash") } } - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button(role: .destructive) { - store.deleteTransaction(transaction) - } label: { - Label("Delete", systemImage: "trash") - } - - Button { - transactionToEdit = transaction - } label: { - Label("Edit", systemImage: "pencil") - } - .tint(.orange) - } } } + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(12) + .padding(.horizontal) } } + .padding(.top) } - .padding(.top) } } } @@ -92,20 +72,6 @@ struct TransactionsView: View { .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Menu { - Picker("Sort by", selection: $sortOrder) { - Label("Latest First", systemImage: "arrow.down").tag(TransactionsStore.SortOrder.dateDescending) - Label("Oldest First", systemImage: "arrow.up").tag(TransactionsStore.SortOrder.dateAscending) - Label("Largest Amount", systemImage: "arrow.down").tag(TransactionsStore.SortOrder.amountDescending) - Label("Smallest Amount", systemImage: "arrow.up").tag(TransactionsStore.SortOrder.amountAscending) - } - } label: { - Image(systemName: "arrow.up.arrow.down") - .foregroundColor(.accentColor) - } - } - - ToolbarItem(placement: .navigationBarTrailing) { Button { showingFilters = true } label: { @@ -130,16 +96,23 @@ struct TransactionsView: View { ) } .sheet(isPresented: $showingAddTransaction) { - AddTransactionView(addTransaction: store.addTransaction) - } - .sheet(item: $transactionToEdit) { transaction in - AddTransactionView( - editingTransaction: transaction, - addTransaction: store.updateTransaction - ) + NavigationView { + AddTransactionView { transaction in + store.addTransaction(transaction) + } + } } .sheet(isPresented: $showingFilters) { - TransactionFilterView(filter: $filter) + NavigationView { + TransactionFilterView(filter: $filter) + } } } } + +struct TransactionsView_Previews: PreviewProvider { + static var previews: some View { + TransactionsView(store: TransactionsStore()) + .environmentObject(AppSettings.shared) + } +} diff --git a/Coinly/UI/Components/TransactionRowView.swift b/Coinly/UI/Components/TransactionRowView.swift index 4bb3841..e4ff336 100644 --- a/Coinly/UI/Components/TransactionRowView.swift +++ b/Coinly/UI/Components/TransactionRowView.swift @@ -9,12 +9,12 @@ struct TransactionRowView: View { } var body: some View { - HStack(spacing: 12) { + HStack(spacing: 16) { // Category Icon ZStack { Circle() - .fill(category.color.opacity(0.1)) - .frame(width: 44, height: 44) + .fill(category.color.opacity(0.15)) + .frame(width: 48, height: 48) Image(systemName: category.icon) .font(.system(size: 20)) @@ -24,10 +24,12 @@ struct TransactionRowView: View { // Transaction Details VStack(alignment: .leading, spacing: 4) { Text(transaction.category) - .font(.headline) + .font(.system(size: 17, weight: .semibold)) + .foregroundColor(Color(uiColor: .label)) + if let note = transaction.note { Text(note) - .font(.subheadline) + .font(.system(size: 15)) .foregroundColor(Color(uiColor: .secondaryLabel)) } } @@ -36,17 +38,25 @@ struct TransactionRowView: View { // Amount and Date VStack(alignment: .trailing, spacing: 4) { - Text(transaction.amountInCurrentCurrency().formatAsCurrency()) - .font(.headline) + Text(transaction.isExpense ? "-" : "+") + .font(.system(size: 17, weight: .semibold)) .foregroundColor(transaction.isExpense ? Color(uiColor: .systemRed) : Color(uiColor: .systemGreen)) + + Text(transaction.amountInCurrentCurrency().formatAsCurrency()) + .font(.system(size: 17, weight: .semibold)) + .foregroundColor(transaction.isExpense ? + Color(uiColor: .systemRed) : + Color(uiColor: .systemGreen)) + Text(transaction.date, style: .date) - .font(.caption) + .font(.system(size: 13)) .foregroundColor(Color(uiColor: .secondaryLabel)) } } - .padding(.vertical, 8) + .padding(.vertical, 12) + .padding(.horizontal, 16) + .background(Color(uiColor: .secondarySystemGroupedBackground)) } }