From 79e649756a8cae86c627ff6111a85d90894a1d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSamoilenkoVadym=E2=80=9D?= <“samoylenko.vadym@gmail.com”> Date: Sun, 2 Mar 2025 20:46:33 +0000 Subject: [PATCH] Fix transaction creation and editing functionality. Update TransactionModel initialization and improve AddTransactionView --- Coinly/Features/Models/TransactionModel.swift | 23 ++- .../Features/Models/TransactionsStore.swift | 68 +++++++- .../Transactions/AddTransactionView.swift | 161 +++++++++++------- .../Transactions/TransactionFilterView.swift | 125 +++++--------- .../Transactions/TransactionsView.swift | 135 +++++++++------ Coinly/UI/Components/CategoryPickerView.swift | 58 +++++++ .../UI/Components/EmptyTransactionsView.swift | 42 +++++ 7 files changed, 392 insertions(+), 220 deletions(-) create mode 100644 Coinly/UI/Components/CategoryPickerView.swift create mode 100644 Coinly/UI/Components/EmptyTransactionsView.swift diff --git a/Coinly/Features/Models/TransactionModel.swift b/Coinly/Features/Models/TransactionModel.swift index 87f29da..fa0604d 100644 --- a/Coinly/Features/Models/TransactionModel.swift +++ b/Coinly/Features/Models/TransactionModel.swift @@ -9,7 +9,20 @@ struct TransactionModel: Identifiable, Codable { var note: String? var originalCurrency: AppSettings.Currency - init(amount: Double, date: Date, type: TransactionType, category: String, note: String?, originalCurrency: AppSettings.Currency) { + var isExpense: Bool { + type == .expense + } + + var signedAmount: Double { + isExpense ? -amount : amount + } + + init(amount: Double, + date: Date, + type: TransactionType, + category: String, + note: String? = nil, + originalCurrency: AppSettings.Currency) { self.id = UUID().uuidString self.amount = amount self.date = date @@ -19,14 +32,6 @@ struct TransactionModel: Identifiable, Codable { self.originalCurrency = originalCurrency } - var isExpense: Bool { - type == .expense - } - - var signedAmount: Double { - isExpense ? -amount : amount - } - func amountInCurrentCurrency() -> Double { let settings = AppSettings.shared return settings.convert(amount, from: originalCurrency, to: settings.currency) diff --git a/Coinly/Features/Models/TransactionsStore.swift b/Coinly/Features/Models/TransactionsStore.swift index 07ea5e2..2d6da7e 100644 --- a/Coinly/Features/Models/TransactionsStore.swift +++ b/Coinly/Features/Models/TransactionsStore.swift @@ -3,22 +3,72 @@ import Foundation class TransactionsStore: ObservableObject { @Published private(set) var transactions: [TransactionModel] = TransactionModel.sampleData - init() { - NotificationCenter.default.addObserver(self, - selector: #selector(currencyDidChange), - name: .currencyDidChange, - object: nil) - } - + // CRUD операции func addTransaction(_ transaction: TransactionModel) { transactions.insert(transaction, at: 0) } + func updateTransaction(_ transaction: TransactionModel) { + if let index = transactions.firstIndex(where: { $0.id == transaction.id }) { + transactions[index] = transaction + } + } + + func deleteTransaction(_ transaction: TransactionModel) { + transactions.removeAll { $0.id == transaction.id } + } + func deleteTransaction(at indexSet: IndexSet) { transactions.remove(atOffsets: indexSet) } - @objc private func currencyDidChange() { - objectWillChange.send() + // Фильтрация + func filterTransactions(searchText: String, filter: TransactionFilter) -> [TransactionModel] { + var filteredTransactions = transactions + + // Фильтр по поисковому запросу + if !searchText.isEmpty { + filteredTransactions = filteredTransactions.filter { transaction in + let searchString = searchText.lowercased() + return transaction.category.lowercased().contains(searchString) || + transaction.note?.lowercased().contains(searchString) ?? false + } + } + + // Фильтр по типу транзакции + if let type = filter.transactionType { + filteredTransactions = filteredTransactions.filter { $0.type == type } + } + + // Фильтр по категориям + if !filter.selectedCategories.isEmpty { + filteredTransactions = filteredTransactions.filter { filter.selectedCategories.contains($0.category) } + } + + // Фильтр по периоду + filteredTransactions = filteredTransactions.filter { filter.period.filter($0.date) } + + return filteredTransactions + } + + // Сортировка + func sortTransactions(_ transactions: [TransactionModel], by sortOrder: SortOrder = .dateDescending) -> [TransactionModel] { + switch sortOrder { + case .dateAscending: + return transactions.sorted { $0.date < $1.date } + case .dateDescending: + return transactions.sorted { $0.date > $1.date } + case .amountAscending: + return transactions.sorted { $0.amount < $1.amount } + case .amountDescending: + return transactions.sorted { $0.amount > $1.amount } + } + } + + enum SortOrder { + case dateAscending + case dateDescending + case amountAscending + case amountDescending } } diff --git a/Coinly/Features/Transactions/AddTransactionView.swift b/Coinly/Features/Transactions/AddTransactionView.swift index 0a064c1..c079395 100644 --- a/Coinly/Features/Transactions/AddTransactionView.swift +++ b/Coinly/Features/Transactions/AddTransactionView.swift @@ -4,20 +4,75 @@ struct AddTransactionView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var settings: AppSettings - @State private var amount = "" - @State private var note = "" - @State private var category = CategoryModel.categories[0].name - @State private var date = Date() - @State private var type: TransactionType = .expense + @State private var amount: String + @State private var note: String + @State private var category: String + @State private var date: Date + @State private var type: TransactionType @State private var showingCategories = false + @State private var showingDeleteConfirmation = false let addTransaction: (TransactionModel) -> Void + let editingTransaction: TransactionModel? + let onDelete: (() -> Void)? + + // Инициализатор для создания новой транзакции + init(addTransaction: @escaping (TransactionModel) -> Void) { + self.addTransaction = addTransaction + self.editingTransaction = nil + self.onDelete = nil + _amount = State(initialValue: "") + _note = State(initialValue: "") + _category = State(initialValue: CategoryModel.categories[0].name) + _date = State(initialValue: Date()) + _type = State(initialValue: .expense) + } + + // Инициализатор для редактирования существующей транзакции + init(editingTransaction: TransactionModel, + addTransaction: @escaping (TransactionModel) -> Void, + onDelete: (() -> Void)? = nil) { + self.addTransaction = addTransaction + self.editingTransaction = editingTransaction + self.onDelete = onDelete + _amount = State(initialValue: String(format: "%.2f", editingTransaction.amount)) + _note = State(initialValue: editingTransaction.note ?? "") + _category = State(initialValue: editingTransaction.category) + _date = State(initialValue: editingTransaction.date) + _type = State(initialValue: editingTransaction.type) + } private var isValidAmount: Bool { guard let amountDouble = Double(amount) else { return false } return amountDouble > 0 } + private func createTransaction() -> TransactionModel? { + guard let amountDouble = Double(amount) else { return nil } + return TransactionModel( + amount: amountDouble, + date: date, + type: type, + category: category, + note: note.isEmpty ? nil : note, + originalCurrency: settings.currency + ) + } + + private var navigationTitle: String { + editingTransaction == nil ? "New Transaction" : "Edit Transaction" + } + + private var saveButtonTitle: String { + editingTransaction == nil ? "Add" : "Save" + } + + private func handleSave() { + guard let transaction = createTransaction() else { return } + addTransaction(transaction) + dismiss() + } + var body: some View { NavigationView { ZStack { @@ -56,9 +111,9 @@ struct AddTransactionView: View { // Details VStack(spacing: 0) { - Button(action: { + Button { showingCategories = true - }) { + } label: { HStack { let selectedCategory = CategoryModel.category(for: category) Image(systemName: selectedCategory.icon) @@ -103,11 +158,28 @@ struct AddTransactionView: View { .background(Color(uiColor: .secondarySystemGroupedBackground)) .cornerRadius(12) .padding(.horizontal) + + if editingTransaction != nil { + Button { + showingDeleteConfirmation = true + } label: { + HStack { + Image(systemName: "trash") + Text("Delete Transaction") + } + .foregroundColor(.red) + .padding() + .frame(maxWidth: .infinity) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(12) + } + .padding(.horizontal) + } } .padding(.top, 20) } } - .navigationTitle("New Transaction") + .navigationTitle(navigationTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { @@ -117,22 +189,21 @@ struct AddTransactionView: View { } ToolbarItem(placement: .confirmationAction) { - Button("Add") { - guard let amountDouble = Double(amount) else { return } - let transaction = TransactionModel( - amount: amountDouble, - date: date, - type: type, - category: category, - note: note.isEmpty ? nil : note, - originalCurrency: settings.currency - ) - addTransaction(transaction) - dismiss() + Button(saveButtonTitle) { + handleSave() } .disabled(!isValidAmount) } } + .alert("Delete Transaction", isPresented: $showingDeleteConfirmation) { + Button("Cancel", role: .cancel) { } + Button("Delete", role: .destructive) { + onDelete?() + dismiss() + } + } message: { + Text("Are you sure you want to delete this transaction? This action cannot be undone.") + } } .sheet(isPresented: $showingCategories) { CategoryPickerView(selectedCategory: $category) @@ -140,57 +211,17 @@ struct AddTransactionView: View { } } -// Вспомогательное view для выбора категории -struct CategoryPickerView: View { - @Environment(\.dismiss) private var dismiss - @Binding var selectedCategory: String - - var body: some View { - NavigationView { - List { - ForEach(CategoryModel.categories) { category in - Button { - selectedCategory = category.name - dismiss() - } label: { - HStack { - Image(systemName: category.icon) - .foregroundColor(Color(category.color)) - .frame(width: 30) - - Text(category.name) - .foregroundColor(AppStyle.labelPrimary) - - Spacer() - - if selectedCategory == category.name { - Image(systemName: "checkmark") - .foregroundColor(.accentColor) - } - } - } - } - } - .navigationTitle("Select Category") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - dismiss() - } - } - } - } - } -} - struct AddTransactionView_Previews: PreviewProvider { static var previews: some View { Group { AddTransactionView(addTransaction: { _ in }) .preferredColorScheme(.light) - AddTransactionView(addTransaction: { _ in }) + AddTransactionView( + editingTransaction: TransactionModel.sampleData[0], + addTransaction: { _ in }, + onDelete: { } + ) .preferredColorScheme(.dark) } .environmentObject(AppSettings.shared) diff --git a/Coinly/Features/Transactions/TransactionFilterView.swift b/Coinly/Features/Transactions/TransactionFilterView.swift index 924204a..014c769 100644 --- a/Coinly/Features/Transactions/TransactionFilterView.swift +++ b/Coinly/Features/Transactions/TransactionFilterView.swift @@ -3,9 +3,8 @@ import SwiftUI struct TransactionFilterView: View { @Environment(\.dismiss) private var dismiss @Binding var filter: TransactionFilter - @State private var selectedPeriodIndex = 0 - private let periods: [(name: String, period: TransactionFilter.Period)] = [ + private let periods: [(title: String, period: TransactionFilter.Period)] = [ ("All Time", .all), ("Today", .today), ("Last 7 Days", .week), @@ -17,14 +16,19 @@ struct TransactionFilterView: View { List { // Period Section Section { - Picker("Time Period", selection: $selectedPeriodIndex) { - ForEach(0..