WIP: Implement add transactions

Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
Billy Brawner 2019-10-10 19:30:43 -07:00
parent 02ca740ad1
commit 3da85c51c3
13 changed files with 231 additions and 18 deletions

View file

@ -13,6 +13,7 @@
284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2841022F2342D97300EAFA29 /* BudgetListsView.swift */; }; 284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2841022F2342D97300EAFA29 /* BudgetListsView.swift */; };
284102322342E12F00EAFA29 /* CategoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284102312342E12F00EAFA29 /* CategoryListView.swift */; }; 284102322342E12F00EAFA29 /* CategoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284102312342E12F00EAFA29 /* CategoryListView.swift */; };
2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2857EAEC233DA30B0026BC83 /* LoadingView.swift */; }; 2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2857EAEC233DA30B0026BC83 /* LoadingView.swift */; };
28A1E95A235006A300CA57FE /* AddTransactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28A1E959235006A300CA57FE /* AddTransactionView.swift */; };
28AC94EE233C373900BFB70A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC94ED233C373900BFB70A /* AppDelegate.swift */; }; 28AC94EE233C373900BFB70A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC94ED233C373900BFB70A /* AppDelegate.swift */; };
28AC94F0233C373900BFB70A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC94EF233C373900BFB70A /* SceneDelegate.swift */; }; 28AC94F0233C373900BFB70A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC94EF233C373900BFB70A /* SceneDelegate.swift */; };
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC94F1233C373900BFB70A /* LoginView.swift */; }; 28AC94F2233C373900BFB70A /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC94F1233C373900BFB70A /* LoginView.swift */; };
@ -64,6 +65,7 @@
2841022F2342D97300EAFA29 /* BudgetListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetListsView.swift; sourceTree = "<group>"; }; 2841022F2342D97300EAFA29 /* BudgetListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetListsView.swift; sourceTree = "<group>"; };
284102312342E12F00EAFA29 /* CategoryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryListView.swift; sourceTree = "<group>"; }; 284102312342E12F00EAFA29 /* CategoryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryListView.swift; sourceTree = "<group>"; };
2857EAEC233DA30B0026BC83 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; }; 2857EAEC233DA30B0026BC83 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
28A1E959235006A300CA57FE /* AddTransactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTransactionView.swift; sourceTree = "<group>"; };
28AC94EA233C373900BFB70A /* Budget.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Budget.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28AC94EA233C373900BFB70A /* Budget.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Budget.app; sourceTree = BUILT_PRODUCTS_DIR; };
28AC94ED233C373900BFB70A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 28AC94ED233C373900BFB70A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
28AC94EF233C373900BFB70A /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; 28AC94EF233C373900BFB70A /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
@ -233,6 +235,7 @@
28FE6B012344331B00D5543E /* TransactionDataStore.swift */, 28FE6B012344331B00D5543E /* TransactionDataStore.swift */,
28FE6B03234449DC00D5543E /* TransactionListView.swift */, 28FE6B03234449DC00D5543E /* TransactionListView.swift */,
28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */, 28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */,
28A1E959235006A300CA57FE /* AddTransactionView.swift */,
); );
path = Transaction; path = Transaction;
sourceTree = "<group>"; sourceTree = "<group>";
@ -389,6 +392,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
28FE6AFE234428BF00D5543E /* DataStoreProvider.swift in Sources */, 28FE6AFE234428BF00D5543E /* DataStoreProvider.swift in Sources */,
28A1E95A235006A300CA57FE /* AddTransactionView.swift in Sources */,
28AC94EE233C373900BFB70A /* AppDelegate.swift in Sources */, 28AC94EE233C373900BFB70A /* AppDelegate.swift in Sources */,
28AC94F0233C373900BFB70A /* SceneDelegate.swift in Sources */, 28AC94F0233C373900BFB70A /* SceneDelegate.swift in Sources */,
28FE6AFC23441E4500D5543E /* CategoryRepository.swift in Sources */, 28FE6AFC23441E4500D5543E /* CategoryRepository.swift in Sources */,

View file

@ -46,8 +46,8 @@
filePath = "Budget/Transaction/TransactionListView.swift" filePath = "Budget/Transaction/TransactionListView.swift"
startingColumnNumber = "9223372036854775807" startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807"
startingLineNumber = "52" startingLineNumber = "53"
endingLineNumber = "52" endingLineNumber = "53"
landmarkName = "body" landmarkName = "body"
landmarkType = "24"> landmarkType = "24">
<Locations> <Locations>
@ -413,8 +413,8 @@
endingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807"
startingLineNumber = "23" startingLineNumber = "23"
endingLineNumber = "23" endingLineNumber = "23"
landmarkName = "getTransactions(_:)" landmarkName = "transaction"
landmarkType = "7"> landmarkType = "24">
</BreakpointContent> </BreakpointContent>
</BreakpointProxy> </BreakpointProxy>
<BreakpointProxy <BreakpointProxy
@ -429,8 +429,8 @@
endingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807"
startingLineNumber = "22" startingLineNumber = "22"
endingLineNumber = "22" endingLineNumber = "22"
landmarkName = "getTransactions(_:)" landmarkName = "transaction"
landmarkType = "7"> landmarkType = "24">
</BreakpointContent> </BreakpointContent>
</BreakpointProxy> </BreakpointProxy>
<BreakpointProxy <BreakpointProxy
@ -513,5 +513,21 @@
landmarkType = "7"> landmarkType = "7">
</BreakpointContent> </BreakpointContent>
</BreakpointProxy> </BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "64E00C07-F5B8-44C5-A75B-76A8F00496E8"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Budget/Transaction/AddTransactionView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "27"
endingLineNumber = "27"
landmarkName = "stateContent"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints> </Breakpoints>
</Bucket> </Bucket>

View file

@ -27,8 +27,8 @@ class DataStoreProvider {
return CategoryDataStore(categoryRepository, budget: budget) return CategoryDataStore(categoryRepository, budget: budget)
} }
func transactionDataStore(_ category: Category? = nil) -> TransactionDataStore { func transactionDataStore() -> TransactionDataStore {
return TransactionDataStore(transactionRepository, category: category) return TransactionDataStore(transactionRepository)
} }
func userDataStore() -> UserDataStore { func userDataStore() -> UserDataStore {

View file

@ -10,6 +10,7 @@ import SwiftUI
struct TabbedBudgetView: View { struct TabbedBudgetView: View {
@ObservedObject var userData: UserDataStore @ObservedObject var userData: UserDataStore
@State var isAddingTransaction = false
var body: some View { var body: some View {
TabView { TabView {
@ -20,18 +21,26 @@ struct TabbedBudgetView: View {
leading: NavigationLink(destination: EmptyView()) { leading: NavigationLink(destination: EmptyView()) {
Text("filter") Text("filter")
}, },
trailing: NavigationLink(destination: EmptyView().navigationBarTitle("add_transaction")) { trailing: Button(action: {
Text("add") self.isAddingTransaction = true
}) {
Image(systemName: "plus")
} }
) )
}.tabItem { .sheet(isPresented: $isAddingTransaction, content: {
AddTransactionView(self.dataStoreProvider)
})
}
.tabItem {
Image(systemName: "dollarsign.circle.fill") Image(systemName: "dollarsign.circle.fill")
Text("transactions") Text("transactions")
} }
BudgetListsView(dataStoreProvider).tabItem { BudgetListsView(dataStoreProvider).tabItem {
Image(systemName: "chart.pie.fill") Image(systemName: "chart.pie.fill")
Text("budgets") Text("budgets")
} }
Text("Profile here").tabItem { Text("Profile here").tabItem {
Image(systemName: "person.circle.fill") Image(systemName: "person.circle.fill")
Text("profile") Text("profile")

View file

@ -0,0 +1,98 @@
//
// AddTransactionView.swift
// Budget
//
// Created by Billy Brawner on 10/10/19.
// Copyright © 2019 William Brawner. All rights reserved.
//
import SwiftUI
struct AddTransactionView: View {
@Environment(\.presentationMode) var presentationMode
@State var id: Int? = nil
@State var title: String = ""
@State var description: String = ""
@State var date: Date = Date()
@State var amount: String = ""
@State var categoryId: Int = 0
@State var type: TransactionType = .expense
@State var budgetId: Int = 0
let createdBy: Int
var stateContent: AnyView {
switch transactionDataStore.transaction {
case .success(_):
// TODO: Figure out how to pass transaction up to previous view
self.presentationMode.wrappedValue.dismiss()
return AnyView(EmptyView())
case .failure(.loading):
return AnyView(VStack {
ActivityIndicator(isAnimating: .constant(true), style: .large)
})
default:
// TODO: Handle each network failure type
return AnyView(Form {
Section {
TextField("prompt_name", text: self.$title)
TextField("prompt_description", text: self.$description)
DatePicker(selection: self.$date, label: { Text("prompt_date") })
TextField("prompt_amount", text: self.$amount)
.keyboardType(.decimalPad)
Picker("prompt_type", selection: self.$type) {
ForEach(TransactionType.allCases) { type in
Text(type.localizedKey)
}
}
// TODO: Figure out how to load budgets dynamically
Picker("prompt_budget", selection: self.$type) {
ForEach(TransactionType.allCases) { type in
Text(type.localizedKey)
}
}
// TODO: Figure out how to load categories dynamically
Picker("prompt_category", selection: self.$type) {
ForEach(TransactionType.allCases) { type in
Text(type.localizedKey)
}
}
}
})
}
}
var body: some View {
NavigationView {
stateContent
.navigationBarTitle("add_transaction")
.navigationBarItems(leading: Button("cancel") {
self.presentationMode.wrappedValue.dismiss()
}, trailing: Button("save") {
self.transactionDataStore.createTransaction(Transaction(
id: self.id,
title: self.title,
description: self.description,
date: self.date,
amount: Int(Double(self.amount) ?? 0.0 * 100.0),
categoryId: self.categoryId,
expense: self.type == TransactionType.expense,
createdBy: self.createdBy,
budgetId: self.budgetId
))
})
}
}
@ObservedObject var transactionDataStore: TransactionDataStore
init(_ dataStoreProvider: DataStoreProvider) {
self.transactionDataStore = dataStoreProvider.transactionDataStore()
self.createdBy = try! dataStoreProvider.userDataStore().currentUser.get().id!
}
}
//struct AddTransactionView_Previews: PreviewProvider {
// static var previews: some View {
// AddTransactionView()
// }
//}

View file

@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import SwiftUI
struct Transaction: Identifiable, Codable { struct Transaction: Identifiable, Codable {
let id: Int? let id: Int?
@ -15,7 +16,25 @@ struct Transaction: Identifiable, Codable {
let date: Date let date: Date
let amount: Int let amount: Int
let categoryId: Int? let categoryId: Int?
let expense: Bool = true let expense: Bool
let createdBy: Int let createdBy: Int
let budgetId: Int let budgetId: Int
} }
enum TransactionType: Int, CaseIterable, Identifiable, Hashable {
case expense
case income
var localizedKey: LocalizedStringKey {
var key: String
switch self {
case .expense:
key = "type_expense"
case .income:
key = "type_income"
}
return LocalizedStringKey(key)
}
var id: TransactionType { self }
}

View file

@ -16,6 +16,12 @@ class TransactionDataStore: ObservableObject {
} }
} }
var transaction: Result<Transaction, NetworkError> = .failure(.unknown) {
didSet {
self.objectWillChange.send()
}
}
func getTransactions(_ category: Category? = nil) { func getTransactions(_ category: Category? = nil) {
self.transactions = .failure(.loading) self.transactions = .failure(.loading)
@ -37,10 +43,26 @@ class TransactionDataStore: ObservableObject {
}) })
} }
func createTransaction(_ transaction: Transaction) {
self.transaction = .failure(.loading)
_ = self.transactionRepository.createTransaction(transaction)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
return
case .failure(let error):
self.transaction = .failure(error)
}
}, receiveValue: { (transaction) in
self.transaction = .success(transaction)
})
}
let objectWillChange = ObservableObjectPublisher() let objectWillChange = ObservableObjectPublisher()
private let transactionRepository: TransactionRepository private let transactionRepository: TransactionRepository
init(_ transactionRepository: TransactionRepository, category: Category? = nil) { init(_ transactionRepository: TransactionRepository) {
self.transactionRepository = transactionRepository self.transactionRepository = transactionRepository
self.getTransactions(category)
} }
} }

View file

@ -9,12 +9,31 @@
import SwiftUI import SwiftUI
struct TransactionDetailsView: View { struct TransactionDetailsView: View {
@State var title: String = ""
@State var description: String? = nil
@State var date: Date = Date()
@State var amount: Int = 0
@State var category: Category? = nil
@State var expense: Bool = true
@State var createdBy: User? = nil
@State var budget: Budget? = nil
var body: some View { var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/) List {
TextField("prompt_title", text: self.$title)
// DatePicker(
}
} }
init(_ dataStoreProvider: DataStoreProvider, transaction: Transaction) { init(_ dataStoreProvider: DataStoreProvider, transaction: Transaction) {
// self.title = transaction.title
// self.description = transaction.description
// self.date = transaction.date
// self.amount = transaction.amount
// self.category = transaction.category
// self.expense = transaction.expense
// self.createdBy = transaction.createdBy
// self.budget = transaction.budget
} }
} }

View file

@ -35,7 +35,8 @@ struct TransactionListView: View {
let dataStoreProvider: DataStoreProvider let dataStoreProvider: DataStoreProvider
init(_ dataStoreProvider: DataStoreProvider, category: Category? = nil) { init(_ dataStoreProvider: DataStoreProvider, category: Category? = nil) {
self.dataStoreProvider = dataStoreProvider self.dataStoreProvider = dataStoreProvider
self.transactionDataStore = dataStoreProvider.transactionDataStore(category) self.transactionDataStore = dataStoreProvider.transactionDataStore()
self.transactionDataStore.getTransactions(category)
} }
} }

View file

@ -19,4 +19,8 @@ class TransactionRepository {
func getTransactions(categoryIds: [Int]? = nil, from: Date? = nil, count: Int? = nil, page: Int? = nil) -> AnyPublisher<[Transaction], NetworkError> { func getTransactions(categoryIds: [Int]? = nil, from: Date? = nil, count: Int? = nil, page: Int? = nil) -> AnyPublisher<[Transaction], NetworkError> {
return apiService.getTransactions(categoryIds: categoryIds, from: from, count: count, page: page) return apiService.getTransactions(categoryIds: categoryIds, from: from, count: count, page: page)
} }
func createTransaction(_ transaction: Transaction) -> AnyPublisher<Transaction, NetworkError> {
return apiService.newTransaction(transaction)
}
} }

View file

@ -9,7 +9,18 @@
// MARK: Generic // MARK: Generic
"add" = "Add"; "add" = "Add";
"filter" = "Filter"; "filter" = "Filter";
"save" = "Save";
"cancel" = "Cancel";
"loading_default" = "Loading..."; "loading_default" = "Loading...";
"prompt_name" = "Name";
"prompt_description" = "Description";
"prompt_amount" = "Amount";
"prompt_date" = "Date";
"prompt_category" = "Category";
"prompt_budget" = "Budget";
"prompt_type" = "Type";
"type_income" = "Income";
"type_expense" = "Expense";
// MARK: Login // MARK: Login
"info_login" = "Login to start managing your budget"; "info_login" = "Login to start managing your budget";

View file

@ -9,7 +9,18 @@
// MARK: Generic // MARK: Generic
"add" = "Agregar"; "add" = "Agregar";
"filter" = "Filtrar"; "filter" = "Filtrar";
"save" = "Guardar";
"cancel" = "Cancelar";
"loading_default" = "Cargando..."; "loading_default" = "Cargando...";
"prompt_name" = "Nombre";
"prompt_description" = "Descripción";
"prompt_amount" = "Monto";
"prompt_date" = "Fecha";
"prompt_category" = "Categoría";
"prompt_budget" = "Presupuesto";
"prompt_type" = "Tipo";
"type_income" = "Ingreso";
"type_expense" = "Gasto";
// MARK: Login // MARK: Login
"info_login" = "Inicia sesión para empezar a manejar su presupuseto"; "info_login" = "Inicia sesión para empezar a manejar su presupuseto";

View file

@ -7,7 +7,6 @@
// //
import XCTest import XCTest
@testable import Budget
class BudgetTests: XCTestCase { class BudgetTests: XCTestCase {