Add ability to edit categories and fix Spanish translations
This commit is contained in:
parent
138a645cce
commit
35f1446e9c
11 changed files with 250 additions and 36 deletions
|
@ -49,6 +49,7 @@
|
|||
28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */; };
|
||||
543ECE42233E82A40018A9D9 /* AuthenticationDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */; };
|
||||
8043EB84271F26ED00498E73 /* CategoryDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8043EB83271F26ED00498E73 /* CategoryDetailsView.swift */; };
|
||||
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -100,8 +101,6 @@
|
|||
28AC950F233C373A00BFB70A /* BudgetUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetUITests.swift; sourceTree = "<group>"; };
|
||||
28AC9511233C373A00BFB70A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
28AC9520233C381C00BFB70A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
28AC9522233C384C00BFB70A /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/LaunchScreen.strings"; sourceTree = "<group>"; };
|
||||
28AC9523233C384C00BFB70A /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
28AC9524233C42D100BFB70A /* TwigsApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsApiService.swift; sourceTree = "<group>"; };
|
||||
28AC9528233C433400BFB70A /* TransactionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionRepository.swift; sourceTree = "<group>"; };
|
||||
28AC952B233C434800BFB70A /* UserRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = "<group>"; };
|
||||
|
@ -120,6 +119,8 @@
|
|||
28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailsView.swift; sourceTree = "<group>"; };
|
||||
543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationDataStore.swift; sourceTree = "<group>"; };
|
||||
8043EB83271F26ED00498E73 /* CategoryDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDetailsView.swift; sourceTree = "<group>"; };
|
||||
809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryFormSheet.swift; sourceTree = "<group>"; };
|
||||
809B94242722597800B1DAE2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -170,11 +171,12 @@
|
|||
2841022A2342D8CB00EAFA29 /* Category */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
284102312342E12F00EAFA29 /* CategoryListView.swift */,
|
||||
28FE6AF723441E1D00D5543E /* Category.swift */,
|
||||
28FE6AF923441E3700D5543E /* CategoryDataStore.swift */,
|
||||
28FE6AFB23441E4500D5543E /* CategoryRepository.swift */,
|
||||
8043EB83271F26ED00498E73 /* CategoryDetailsView.swift */,
|
||||
284102312342E12F00EAFA29 /* CategoryListView.swift */,
|
||||
28FE6AFB23441E4500D5543E /* CategoryRepository.swift */,
|
||||
809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */,
|
||||
);
|
||||
path = Category;
|
||||
sourceTree = "<group>";
|
||||
|
@ -212,27 +214,27 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
282126A4235BCB7500072D52 /* Twigs.entitlements */,
|
||||
2821269F2359299D00072D52 /* Profile */,
|
||||
2841022A2342D8CB00EAFA29 /* Category */,
|
||||
284102292342D8BB00EAFA29 /* Budget */,
|
||||
2857EAEB233DA2F90026BC83 /* Views */,
|
||||
28AC952A233C433C00BFB70A /* User */,
|
||||
28AC9527233C430A00BFB70A /* Network */,
|
||||
28AC9526233C42F800BFB70A /* Transaction */,
|
||||
28AC94ED233C373900BFB70A /* AppDelegate.swift */,
|
||||
28AC94EF233C373900BFB70A /* SceneDelegate.swift */,
|
||||
28AC94F1233C373900BFB70A /* LoginView.swift */,
|
||||
28B9E50D2346BCB2007C3909 /* RegistrationView.swift */,
|
||||
2841022623419A2B00EAFA29 /* TabbedBudgetView.swift */,
|
||||
284102242341998300EAFA29 /* ContentView.swift */,
|
||||
28AC9521233C381C00BFB70A /* Localizable.strings */,
|
||||
28AC94F3233C373A00BFB70A /* Assets.xcassets */,
|
||||
28AC94F8233C373A00BFB70A /* LaunchScreen.storyboard */,
|
||||
28AC94FB233C373A00BFB70A /* Info.plist */,
|
||||
28AC94F5233C373A00BFB70A /* Preview Content */,
|
||||
28AC94ED233C373900BFB70A /* AppDelegate.swift */,
|
||||
284102242341998300EAFA29 /* ContentView.swift */,
|
||||
28FE6AFD234428BF00D5543E /* DataStoreProvider.swift */,
|
||||
2888234623512DBF003D3847 /* Observable.swift */,
|
||||
28CE8B9423525F990072BC4C /* Extensions.swift */,
|
||||
28AC94F1233C373900BFB70A /* LoginView.swift */,
|
||||
2888234623512DBF003D3847 /* Observable.swift */,
|
||||
28B9E50D2346BCB2007C3909 /* RegistrationView.swift */,
|
||||
28AC94EF233C373900BFB70A /* SceneDelegate.swift */,
|
||||
2841022623419A2B00EAFA29 /* TabbedBudgetView.swift */,
|
||||
28AC94F3233C373A00BFB70A /* Assets.xcassets */,
|
||||
284102292342D8BB00EAFA29 /* Budget */,
|
||||
2841022A2342D8CB00EAFA29 /* Category */,
|
||||
28AC94F8233C373A00BFB70A /* LaunchScreen.storyboard */,
|
||||
28AC9521233C381C00BFB70A /* Localizable.strings */,
|
||||
28AC9527233C430A00BFB70A /* Network */,
|
||||
28AC94F5233C373A00BFB70A /* Preview Content */,
|
||||
2821269F2359299D00072D52 /* Profile */,
|
||||
28AC9526233C42F800BFB70A /* Transaction */,
|
||||
28AC952A233C433C00BFB70A /* User */,
|
||||
2857EAEB233DA2F90026BC83 /* Views */,
|
||||
);
|
||||
path = Twigs;
|
||||
sourceTree = "<group>";
|
||||
|
@ -384,7 +386,7 @@
|
|||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
"es-419",
|
||||
es,
|
||||
);
|
||||
mainGroup = 28AC94E1233C373900BFB70A;
|
||||
productRefGroup = 28AC94EB233C373900BFB70A /* Products */;
|
||||
|
@ -460,6 +462,7 @@
|
|||
28FE6AF823441E1D00D5543E /* Category.swift in Sources */,
|
||||
282126A1235929B800072D52 /* ProfileView.swift in Sources */,
|
||||
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */,
|
||||
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */,
|
||||
28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */,
|
||||
28FE6B022344331B00D5543E /* TransactionDataStore.swift in Sources */,
|
||||
543ECE42233E82A40018A9D9 /* AuthenticationDataStore.swift in Sources */,
|
||||
|
@ -506,7 +509,6 @@
|
|||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
28AC94F9233C373A00BFB70A /* Base */,
|
||||
28AC9522233C384C00BFB70A /* es-419 */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
|
@ -515,7 +517,7 @@
|
|||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
28AC9520233C381C00BFB70A /* en */,
|
||||
28AC9523233C384C00BFB70A /* es-419 */,
|
||||
809B94242722597800B1DAE2 /* es */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
@ -12,12 +12,7 @@ import Combine
|
|||
class CategoryDataStore: ObservableObject {
|
||||
private var currentRequest: AnyCancellable? = nil
|
||||
@Published var categories: [String:Result<[Category], NetworkError>] = ["":.failure(.loading)]
|
||||
|
||||
var category: Result<Category, NetworkError> = .failure(.loading) {
|
||||
didSet {
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
@Published var category: Result<Category, NetworkError> = .failure(.unknown)
|
||||
|
||||
func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = false, count: Int? = nil, page: Int? = nil) -> String {
|
||||
let requestId = "\(budgetId ?? "all")-\(String(describing: expense))-\(String(describing: archived))"
|
||||
|
@ -59,8 +54,52 @@ class CategoryDataStore: ObservableObject {
|
|||
self.category = .success(category)
|
||||
})
|
||||
}
|
||||
|
||||
let objectWillChange = ObservableObjectPublisher()
|
||||
|
||||
func selectCategory(_ category: Category) {
|
||||
self.category = .success(category)
|
||||
}
|
||||
|
||||
func save(_ category: Category) {
|
||||
self.category = .failure(.loading)
|
||||
|
||||
var savePublisher: AnyPublisher<Category, NetworkError>
|
||||
if (category.id != "") {
|
||||
savePublisher = self.categoryRepository.updateCategory(category)
|
||||
} else {
|
||||
savePublisher = self.categoryRepository.createCategory(category)
|
||||
}
|
||||
self.currentRequest = savePublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { (completion) in
|
||||
switch completion {
|
||||
case .finished:
|
||||
self.currentRequest = nil
|
||||
return
|
||||
case .failure(let error):
|
||||
self.category = .failure(error)
|
||||
}
|
||||
}, receiveValue: { (category) in
|
||||
self.category = .success(category)
|
||||
})
|
||||
}
|
||||
|
||||
func delete(_ id: String) {
|
||||
self.category = .failure(.loading)
|
||||
self.currentRequest = self.categoryRepository.deleteCategory(id)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { (completion) in
|
||||
switch completion {
|
||||
case .finished:
|
||||
self.currentRequest = nil
|
||||
return
|
||||
case .failure(let error):
|
||||
self.category = .failure(error)
|
||||
}
|
||||
}, receiveValue: { _ in
|
||||
self.category = .failure(.deleted)
|
||||
})
|
||||
}
|
||||
|
||||
private let categoryRepository: CategoryRepository
|
||||
init(_ categoryRepository: CategoryRepository) {
|
||||
self.categoryRepository = categoryRepository
|
||||
|
|
|
@ -13,6 +13,7 @@ struct CategoryDetailsView: View {
|
|||
let budget: Budget
|
||||
let category: Category
|
||||
@State var sumRequest: String = ""
|
||||
@State var editingCategory: Bool = false
|
||||
var spent: Int {
|
||||
get {
|
||||
if case let .success(res) = transactionDataStore.sums[sumRequest] {
|
||||
|
@ -53,6 +54,17 @@ struct CategoryDetailsView: View {
|
|||
sumRequest = transactionDataStore.sum(budgetId: nil, categoryId: category.id, from: nil, to: nil)
|
||||
}
|
||||
}
|
||||
.navigationBarItems(trailing: Button(action: {
|
||||
self.editingCategory = true
|
||||
}) {
|
||||
Text("edit")
|
||||
}
|
||||
)
|
||||
.sheet(isPresented: self.$editingCategory, onDismiss: {
|
||||
self.editingCategory = false
|
||||
}, content: {
|
||||
CategoryFormSheet(showSheet: self.$editingCategory, category: self.category, budgetId: self.category.budgetId)
|
||||
})
|
||||
}
|
||||
|
||||
init (_ category: Category, budget: Budget) {
|
||||
|
|
108
Twigs/Category/CategoryFormSheet.swift
Normal file
108
Twigs/Category/CategoryFormSheet.swift
Normal file
|
@ -0,0 +1,108 @@
|
|||
//
|
||||
// EditCategoryView.swift
|
||||
// Twigs
|
||||
//
|
||||
// Created by William Brawner on 10/21/21.
|
||||
// Copyright © 2021 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CategoryFormSheet: View {
|
||||
@EnvironmentObject var categoryDataStore: CategoryDataStore
|
||||
@Binding var showSheet: Bool
|
||||
@State var title: String
|
||||
@State var description: String
|
||||
@State var amount: String
|
||||
@State var type: TransactionType
|
||||
@State var archived: Bool
|
||||
let categoryId: String
|
||||
let budgetId: String
|
||||
@State private var showingAlert = false
|
||||
|
||||
var stateContent: AnyView {
|
||||
switch categoryDataStore.category {
|
||||
case .success(_):
|
||||
self.showSheet = false
|
||||
return AnyView(EmptyView())
|
||||
case .failure(.loading):
|
||||
return AnyView(EmbeddedLoadingView())
|
||||
default:
|
||||
return AnyView(Form {
|
||||
TextField("prompt_name", text: self.$title)
|
||||
TextField("prompt_description", text: self.$description)
|
||||
TextField("prompt_amount", text: self.$amount)
|
||||
.keyboardType(.decimalPad)
|
||||
Picker("prompt_type", selection: self.$type) {
|
||||
ForEach(TransactionType.allCases) { type in
|
||||
Text(type.localizedKey)
|
||||
}
|
||||
}
|
||||
Toggle("prompt_archived", isOn: self.$archived)
|
||||
if categoryId != "" {
|
||||
Button(action: {
|
||||
self.showingAlert = true
|
||||
}) {
|
||||
Text("delete")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.alert(isPresented:$showingAlert) {
|
||||
Alert(title: Text("confirm_delete"), message: Text("cannot_undo"), primaryButton: .destructive(Text("delete"), action: {
|
||||
self.categoryDataStore.delete(categoryId)
|
||||
}), secondaryButton: .cancel())
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
stateContent
|
||||
.navigationBarItems(
|
||||
leading: Button("cancel") {
|
||||
self.showSheet = false
|
||||
},
|
||||
trailing: Button("save") {
|
||||
let amount = Double(self.amount) ?? 0.0
|
||||
self.categoryDataStore.save(Category(
|
||||
budgetId: self.budgetId,
|
||||
id: self.categoryId,
|
||||
title: self.title,
|
||||
description: self.description,
|
||||
amount: Int(amount * 100.0),
|
||||
expense: self.type == TransactionType.expense,
|
||||
archived: false
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
init(showSheet: Binding<Bool>, category: Category?, budgetId: String) {
|
||||
let initialCategory = category ?? Category(budgetId: budgetId, id: "", title: "", description: "", amount: 0, expense: true, archived: false)
|
||||
self._showSheet = showSheet
|
||||
self._title = State(initialValue: initialCategory.title)
|
||||
self._description = State(initialValue: initialCategory.description ?? "")
|
||||
self._amount = State(initialValue: initialCategory.amount.toDecimalString())
|
||||
let type: TransactionType
|
||||
if initialCategory.expense == false {
|
||||
type = .income
|
||||
} else {
|
||||
type = .expense
|
||||
}
|
||||
self._type = State(initialValue: type)
|
||||
self._archived = State(initialValue: initialCategory.archived)
|
||||
self.categoryId = initialCategory.id
|
||||
self.budgetId = budgetId
|
||||
}
|
||||
}
|
||||
|
||||
struct CategoryFormSheet_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CategoryFormSheet(showSheet: .constant(true), category: nil, budgetId: "")
|
||||
.environmentObject(CategoryDataStore(MockCategoryRepository()))
|
||||
}
|
||||
}
|
|
@ -12,6 +12,9 @@ import Combine
|
|||
protocol CategoryRepository {
|
||||
func getCategories(budgetId: String?, expense: Bool?, archived: Bool?, count: Int?, page: Int?) -> AnyPublisher<[Category], NetworkError>
|
||||
func getCategory(_ categoryId: String) -> AnyPublisher<Category, NetworkError>
|
||||
func createCategory(_ category: Category) -> AnyPublisher<Category, NetworkError>
|
||||
func updateCategory(_ category: Category) -> AnyPublisher<Category, NetworkError>
|
||||
func deleteCategory(_ id: String) -> AnyPublisher<Empty, NetworkError>
|
||||
}
|
||||
|
||||
class NetworkCategoryRepository: CategoryRepository {
|
||||
|
@ -47,6 +50,27 @@ class NetworkCategoryRepository: CategoryRepository {
|
|||
return category
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func createCategory(_ category: Category) -> AnyPublisher<Category, NetworkError> {
|
||||
return apiService.newCategory(category).map {
|
||||
self.cacheService?.addCategory($0)
|
||||
return $0
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func updateCategory(_ category: Category) -> AnyPublisher<Category, NetworkError> {
|
||||
return apiService.updateCategory(category).map {
|
||||
self.cacheService?.updateCategory($0)
|
||||
return $0
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func deleteCategory(_ id: String) -> AnyPublisher<Empty, NetworkError> {
|
||||
return apiService.deleteCategory(id).map {
|
||||
self.cacheService?.removeCategory(id)
|
||||
return $0
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
@ -69,6 +93,18 @@ class MockCategoryRepository: CategoryRepository {
|
|||
func getCategory(_ categoryId: String) -> AnyPublisher<Category, NetworkError> {
|
||||
return Result.Publisher(MockCategoryRepository.category).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func createCategory(_ category: Category) -> AnyPublisher<Category, NetworkError> {
|
||||
return Result.Publisher(MockCategoryRepository.category).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func updateCategory(_ category: Category) -> AnyPublisher<Category, NetworkError> {
|
||||
return Result.Publisher(MockCategoryRepository.category).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func deleteCategory(_ id: String) -> AnyPublisher<Empty, NetworkError> {
|
||||
return Result.Publisher(.success(Empty())).eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
import Foundation
|
||||
|
||||
extension Int {
|
||||
func toDecimalString() -> String {
|
||||
return String(format: "%.2f", Double(self) / 100.0)
|
||||
}
|
||||
|
||||
func toCurrencyString() -> String {
|
||||
let currencyFormatter = NumberFormatter()
|
||||
currencyFormatter.locale = Locale.current
|
||||
|
|
|
@ -315,7 +315,7 @@ class RequestHelper {
|
|||
default: throw NetworkError.unknown
|
||||
}
|
||||
}
|
||||
print(String(data: data, encoding: String.Encoding.utf8))
|
||||
// print(String(data: data, encoding: String.Encoding.utf8))
|
||||
return data
|
||||
}
|
||||
.decode(type: ResultType.self, decoder: self.decoder)
|
||||
|
|
|
@ -97,7 +97,19 @@ class TwigsInMemoryCacheService {
|
|||
func addCategory(_ category: Category) {
|
||||
self.categories.insert(category)
|
||||
}
|
||||
|
||||
func updateCategory(_ category: Category) {
|
||||
if let index = self.categories.firstIndex(where: { $0.id == category.id }) {
|
||||
self.categories.remove(at: index)
|
||||
}
|
||||
self.categories.insert(category)
|
||||
}
|
||||
|
||||
func removeCategory(_ id: String) {
|
||||
if let index = self.categories.firstIndex(where: { $0.id == id }) {
|
||||
self.categories.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Users
|
||||
func getUser(id: String) -> AnyPublisher<User, NetworkError>? {
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"prompt_category" = "Category";
|
||||
"prompt_budget" = "Budget";
|
||||
"prompt_type" = "Type";
|
||||
"prompt_archived" = "Archived";
|
||||
"type_income" = "Income";
|
||||
"type_expense" = "Expense";
|
||||
"retry" = "Retry";
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
Localizable.strings
|
||||
Budget
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
|||
"prompt_category" = "Categoría";
|
||||
"prompt_budget" = "Presupuesto";
|
||||
"prompt_type" = "Tipo";
|
||||
"prompt_archived" = "Archivada";
|
||||
"type_income" = "Ingreso";
|
||||
"type_expense" = "Gasto";
|
||||
"retry" = "Volver a intentar";
|
Loading…
Reference in a new issue