Add read-only view of recurring transactions

This commit is contained in:
William Brawner 2021-12-07 20:22:06 -07:00
parent 733f1cc764
commit b035b027ab
13 changed files with 618 additions and 14 deletions

View file

@ -45,11 +45,16 @@
28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B03234449DC00D5543E /* TransactionListView.swift */; };
28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */; };
543ECE42233E82A40018A9D9 /* AuthenticationDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */; };
801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */; };
801D08CE275F189E00931465 /* RecurringTransactionsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */; };
801D08D0275F1AE300931465 /* RecurringTransactionDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */; };
801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */; };
8043EB84271F26ED00498E73 /* CategoryDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8043EB83271F26ED00498E73 /* CategoryDetailsView.swift */; };
806C7850272B700B00FA1375 /* TwigsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806C784F272B700B00FA1375 /* TwigsApp.swift */; };
80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80820144275FFD380040996E /* SidebarBudgetView.swift */; };
8094A9C327567CAC006C6C62 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 8094A9C227567CAC006C6C62 /* Collections */; };
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */; };
80D6B1F1275B11DE0075D0EC /* RecurringTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D6B1F0275B11DE0075D0EC /* RecurringTransaction.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -115,11 +120,16 @@
28FE6B03234449DC00D5543E /* TransactionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionListView.swift; sourceTree = "<group>"; };
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>"; };
801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsListView.swift; sourceTree = "<group>"; };
801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsRepository.swift; sourceTree = "<group>"; };
801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionDataStore.swift; sourceTree = "<group>"; };
801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionDetailsView.swift; sourceTree = "<group>"; };
8043EB83271F26ED00498E73 /* CategoryDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDetailsView.swift; sourceTree = "<group>"; };
806C784F272B700B00FA1375 /* TwigsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsApp.swift; sourceTree = "<group>"; };
80820144275FFD380040996E /* SidebarBudgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarBudgetView.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>"; };
80D6B1F0275B11DE0075D0EC /* RecurringTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransaction.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -231,6 +241,7 @@
28AC9527233C430A00BFB70A /* Network */,
28AC94F5233C373A00BFB70A /* Preview Content */,
2821269F2359299D00072D52 /* Profile */,
80D6B1EF275B11C10075D0EC /* Recurring Transactions */,
28AC9526233C42F800BFB70A /* Transaction */,
28AC952A233C433C00BFB70A /* User */,
2857EAEB233DA2F90026BC83 /* Views */,
@ -299,6 +310,18 @@
path = User;
sourceTree = "<group>";
};
80D6B1EF275B11C10075D0EC /* Recurring Transactions */ = {
isa = PBXGroup;
children = (
80D6B1F0275B11DE0075D0EC /* RecurringTransaction.swift */,
801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */,
801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */,
801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */,
801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */,
);
path = "Recurring Transactions";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -444,13 +467,16 @@
282126622357E45F00072D52 /* TransactionEditView.swift in Sources */,
28FE6AFC23441E4500D5543E /* CategoryRepository.swift in Sources */,
2841022C2342D8E400EAFA29 /* Budget.swift in Sources */,
801D08CE275F189E00931465 /* RecurringTransactionsRepository.swift in Sources */,
2841022723419A2B00EAFA29 /* TabbedBudgetView.swift in Sources */,
801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */,
28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */,
28FE6AF42342E3CB00D5543E /* BudgetsDataStore.swift in Sources */,
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */,
28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */,
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */,
282126BB235CDD3C00072D52 /* BudgetDetailsView.swift in Sources */,
801D08D0275F1AE300931465 /* RecurringTransactionDataStore.swift in Sources */,
28AC9525233C42D100BFB70A /* TwigsApiService.swift in Sources */,
2888234723512DBF003D3847 /* Observable.swift in Sources */,
2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */,
@ -466,6 +492,7 @@
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */,
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */,
28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */,
80D6B1F1275B11DE0075D0EC /* RecurringTransaction.swift in Sources */,
28FE6B022344331B00D5543E /* TransactionDataStore.swift in Sources */,
543ECE42233E82A40018A9D9 /* AuthenticationDataStore.swift in Sources */,
80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */,
@ -474,6 +501,7 @@
806C7850272B700B00FA1375 /* TwigsApp.swift in Sources */,
28AC952E233C43A300BFB70A /* User.swift in Sources */,
28CE8B9523525F990072BC4C /* Extensions.swift in Sources */,
801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -581,7 +609,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.7;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -638,7 +666,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.7;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "";

View file

@ -62,11 +62,6 @@ struct BudgetDetailsView: View {
}
}
}.listStyle(.insetGrouped)
.navigationBarItems(leading: HStack {
Button("budgets", action: {
self.budgetDataStore.showBudgetSelection = true
}).padding()
})
default:
Text("An error has ocurred")
}

View file

@ -37,7 +37,7 @@ struct BudgetListsView: View {
struct BudgetListItemView: View {
@EnvironmentObject var budgetDataStore: BudgetsDataStore
let budget: Budget
var body: some View {
Button(
action: {
@ -58,7 +58,7 @@ struct BudgetListItemView: View {
}
)
}
init (_ budget: Budget) {
self.budget = budget
}

View file

@ -34,12 +34,15 @@ struct LoginView: View {
Text("info_login")
TextField(LocalizedStringKey("prompt_server"), text: self.$server)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textContentType(.URL)
TextField(LocalizedStringKey("prompt_username"), text: self.$username)
.autocapitalization(UITextAutocapitalizationType.none)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textContentType(.username)
SecureField(LocalizedStringKey("prompt_password"), text: self.$password, prompt: nil)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textContentType(UITextContentType.password)
.textContentType(.password)
Button("action_login", action: {
self.userData.login(server: self.server, username: self.username, password: self.password)
}).buttonStyle(DefaultButtonStyle())

View file

@ -9,7 +9,7 @@
import Foundation
import Combine
class TwigsApiService {
class TwigsApiService: RecurringTransactionsRepository {
let requestHelper: RequestHelper
init(_ requestHelper: RequestHelper) {
@ -207,6 +207,27 @@ class TwigsApiService {
func deleteUser(_ user: User) -> AnyPublisher<Empty, NetworkError> {
return requestHelper.delete("/api/users/\(user.id)")
}
// MARK: Recurring Transactions
func getRecurringTransactions(budgetId: String) -> AnyPublisher<[RecurringTransaction], NetworkError> {
return requestHelper.get("/api/recurringtransactions", queries: ["budgetId": [budgetId]])
}
func getRecurringTransaction(_ id: String) -> AnyPublisher<RecurringTransaction, NetworkError> {
return requestHelper.get("/api/recurringtransactions/\(id)")
}
func createRecurringTransaction(_ transaction: RecurringTransaction) -> AnyPublisher<RecurringTransaction, NetworkError> {
return requestHelper.post("/api/recurringtransactions", data: transaction, type: RecurringTransaction.self)
}
func updateRecurringTransaction(_ transaction: RecurringTransaction) -> AnyPublisher<RecurringTransaction, NetworkError> {
return requestHelper.put("/api/recurringtransactions/\(transaction.id)", data: transaction)
}
func deleteRecurringTransaction(_ id: String) -> AnyPublisher<Empty, NetworkError> {
return requestHelper.delete("/api/recurringtransactions/\(id)")
}
}
private let BASE_URL = "BASE_URL"

View file

@ -0,0 +1,259 @@
//
// RecurringTransaction.swift
// Twigs
//
// Created by William Brawner on 12/3/21.
// Copyright © 2021 William Brawner. All rights reserved.
//
import Foundation
struct RecurringTransaction: Identifiable, Hashable, Codable {
let id: String
let title: String
let description: String?
let frequency: Frequency
let start: Date
let end: Date?
let amount: Int
let categoryId: String?
let expense: Bool
let createdBy: String
let budgetId: String
}
struct Frequency: Hashable, Codable, CustomStringConvertible {
let unit: FrequencyUnit
let count: Int
let time: Time
init?(unit: FrequencyUnit, count: Int, time: Time) {
if count < 1 {
return nil
}
self.unit = unit
self.count = count
self.time = time
}
init?(from string: String) {
let parts = string.split(separator: ";")
guard let count = Int(parts[1]) else {
return nil
}
var timeIndex = 3
switch parts[0] {
case "D":
self.unit = .daily
timeIndex = 2
case "W":
let daysOfWeek = parts[2].split(separator: ",").compactMap { dayOfWeek in
DayOfWeek(rawValue: String(dayOfWeek))
}
if daysOfWeek.isEmpty {
return nil
}
self.unit = .weekly(Set(daysOfWeek))
case "M":
guard let dayOfMonth = DayOfMonth(from: String(parts[2])) else {
return nil
}
self.unit = .monthly(dayOfMonth)
case "Y":
guard let dayOfYear = DayOfYear(from: String(parts[2])) else {
return nil
}
self.unit = .yearly(dayOfYear)
default:
return nil
}
guard let time = Time(from: String(parts[timeIndex])) else {
return nil
}
self.time = time
self.count = count
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let frequencyString = try container.decode(String.self)
self.init(from: frequencyString)!
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(description)
}
var description: String {
// TODO: Make the backend representation of this more sensible and then use this
// return [unit.description, count.description, time.description].joined(separator: ";")
let unitParts = "\(unit)".split(separator: ";")
if unitParts.count == 1 {
return [unitParts[0].description, count.description, time.description].joined(separator: ";")
} else{
return [unitParts[0].description, count.description, unitParts[1].description, time.description].joined(separator: ";")
}
}
}
enum FrequencyUnit: Hashable, CustomStringConvertible {
case daily
case weekly(Set<DayOfWeek>)
case monthly(DayOfMonth)
case yearly(DayOfYear)
var description: String {
switch self {
case .daily:
return "D"
case .weekly(let daysOfWeek):
return String(format: "W;%s", daysOfWeek.map { $0.rawValue }.joined(separator: ","))
case .monthly(let dayOfMonth):
return String(format: "M;%s", dayOfMonth.description)
case .yearly(let dayOfYear):
return String(format: "Y;%s", dayOfYear.description)
}
}
}
struct Time: Hashable, CustomStringConvertible {
let hours: Int
let minutes: Int
let seconds: Int
init?(hours: Int, minutes: Int, seconds: Int) {
if hours < 0 || hours > 23 {
return nil
}
if minutes < 0 || minutes > 59 {
return nil
}
if seconds < 0 || seconds > 59 {
return nil
}
self.hours = hours
self.minutes = minutes
self.seconds = seconds
}
init?(from string: String) {
let parts = string.split(separator: ":").compactMap {
Int($0)
}
if parts.count != 3 {
return nil
}
self.init(hours: parts[0], minutes: parts[1], seconds: parts[2])
}
var description: String {
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}
}
enum DayOfMonth: Hashable, CustomStringConvertible {
case positional(Position, DayOfWeek)
case fixed(Int)
init?(position: Position, dayOfWeek: DayOfWeek) {
if position == .day {
return nil
}
self = .positional(position, dayOfWeek)
}
init?(day: Int) {
if day < 1 || day > 31 {
return nil
}
self = .fixed(day)
}
init?(from string: String) {
let parts = string.split(separator: "-")
guard let position = Position.init(rawValue: String(parts[0])) else {
return nil
}
if position == .day {
guard let day = Int(parts[1]) else {
return nil
}
self = .fixed(day)
} else {
guard let dayOfWeek = DayOfWeek(rawValue: String(parts[1])) else {
return nil
}
self = .positional(position, dayOfWeek)
}
}
var description: String {
switch self {
case .positional(let position, let dayOfWeek):
return "\(position)-\(dayOfWeek)"
case .fixed(let day):
return "\(Position.day)-\(day)"
}
}
}
enum Position: String, Hashable {
case day = "DAY"
case first = "FIRST"
case second = "SECOND"
case third = "THIRD"
case fourth = "FOURTH"
case last = "LAST"
}
enum DayOfWeek: String, Hashable {
case monday = "MONDAY"
case tuesday = "TUESDAY"
case wednesday = "WEDNESDAY"
case thursday = "THURSDAY"
case friday = "FRIDAY"
case saturday = "SATURDAY"
case sunday = "SUNDAY"
}
struct DayOfYear: Hashable, CustomStringConvertible {
let month: Int
let day: Int
init?(month: Int, day: Int) {
var maxDay: Int
switch month {
case 2:
maxDay = 29;
break;
case 4, 6, 9, 11:
maxDay = 30;
break;
default:
maxDay = 31;
}
if day < 1 || day > maxDay {
return nil
}
if month < 1 || month > 12 {
return nil
}
self.day = day
self.month = month
}
init?(from string: String) {
let parts = string.split(separator: "-").compactMap {
Int($0)
}
if parts.count < 2 {
return nil
}
self.init(month: parts[0], day: parts[1])
}
var description: String {
return String(format: "%02d-%02d", self.month, self.day)
}
}

View file

@ -0,0 +1,111 @@
//
// RecurringTransactionDataStore.swift
// Twigs
//
// Created by William Brawner on 12/6/21.
// Copyright © 2021 William Brawner. All rights reserved.
//
import Foundation
import Combine
import Collections
class RecurringTransactionDataStore: ObservableObject {
private let repository: RecurringTransactionsRepository
private var currentRequest: AnyCancellable? = nil
@Published var transactions: Result<[RecurringTransaction], NetworkError>? = nil
@Published var transaction: Result<RecurringTransaction, NetworkError>? = nil
init(_ repository: RecurringTransactionsRepository, budgetId: String) {
self.repository = repository
getRecurringTransactions(budgetId)
}
func getRecurringTransactions(_ budgetId: String) {
self.transactions = .failure(.loading)
self.currentRequest = self.repository.getRecurringTransactions(budgetId: budgetId)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
self.currentRequest = nil
return
case .failure(let error):
print("Error loading recurring transactions: \(error.name)")
self.transactions = .failure(error)
}
}, receiveValue: { (transactions) in
self.transactions = .success(transactions.sorted(by: { $0.title < $1.title }))
})
}
func getRecurringTransaction(_ id: String) {
self.transaction = .failure(.loading)
self.currentRequest = self.repository.getRecurringTransaction(id)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
self.currentRequest = nil
return
case .failure(let error):
self.transaction = .failure(error)
}
}, receiveValue: { (transaction) in
self.transaction = .success(transaction)
})
}
func saveRecurringTransaction(_ transaction: RecurringTransaction) {
self.transaction = .failure(.loading)
var transactionSavePublisher: AnyPublisher<RecurringTransaction, NetworkError>
if (transaction.id != "") {
transactionSavePublisher = self.repository.updateRecurringTransaction(transaction)
} else {
transactionSavePublisher = self.repository.createRecurringTransaction(transaction)
}
self.currentRequest = transactionSavePublisher
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
self.currentRequest = nil
return
case .failure(let error):
self.transaction = .failure(error)
}
}, receiveValue: { (transaction) in
self.transaction = .success(transaction)
if case var .success(transactions) = self.transactions {
transactions.insert(transaction, at: 0)
self.transactions = .success(transactions)
}
})
}
func deleteRecurringTransaction(_ id: String) {
self.transaction = .failure(.loading)
self.currentRequest = self.repository.deleteRecurringTransaction(id)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
self.currentRequest = nil
return
case .failure(let error):
self.transaction = .failure(error)
}
}, receiveValue: { (empty) in
self.transaction = .failure(.deleted)
if case let .success(transactions) = self.transactions {
self.transactions = .success(transactions.filter { $0.id != id })
}
})
}
func clearSelectedRecurringTransaction() {
self.transaction = nil
}
}

View file

@ -0,0 +1,47 @@
//
// RecurringTransactionDetailsView.swift
// Twigs
//
// Created by William Brawner on 12/7/21.
// Copyright © 2021 William Brawner. All rights reserved.
//
import SwiftUI
struct RecurringTransactionDetailsView: View {
let transaction: RecurringTransaction
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text(transaction.title)
.font(.title)
Text(transaction.amount.toCurrencyString())
.font(.headline)
.foregroundColor(transaction.expense ? .red : .green)
.multilineTextAlignment(.trailing)
if let description = transaction.description {
Text(description)
}
Spacer().frame(height: 10)
LabeledField(label: "start", value: transaction.start.toLocaleString(), showDivider: true)
LabeledField(label: "end", value: transaction.end?.toLocaleString(), showDivider: true)
CategoryLineItem(transaction.categoryId)
BudgetLineItem()
UserLineItem(transaction.createdBy)
}.padding()
}
}
init(_ transaction: RecurringTransaction) {
self.transaction = transaction
}
}
#if DEBUG
struct RecurringTransactionDetailsView_Previews: PreviewProvider {
static var previews: some View {
RecurringTransactionDetailsView(MockRecurringTransactionRepository.transaction)
}
}
#endif

View file

@ -0,0 +1,75 @@
//
// RecurringTransactionView.swift
// Twigs
//
// Created by William Brawner on 12/6/21.
// Copyright © 2021 William Brawner. All rights reserved.
//
import SwiftUI
struct RecurringTransactionsListView: View {
@ObservedObject var dataStore: RecurringTransactionDataStore
var body: some View {
switch dataStore.transactions {
case .success(let transactions):
List {
ForEach(transactions) { transaction in
RecurringTransactionsListItemView(transaction)
}
}
default:
ActivityIndicator(isAnimating: .constant(true), style: .medium)
}
}
}
struct RecurringTransactionView_Previews: PreviewProvider {
static var previews: some View {
RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(MockRecurringTransactionRepository(), budgetId: ""))
}
}
struct RecurringTransactionsListItemView: View {
let transaction: RecurringTransaction
init (_ transaction: RecurringTransaction) {
self.transaction = transaction
}
var body: some View {
NavigationLink(
destination: RecurringTransactionDetailsView(transaction)
.navigationBarTitle("details", displayMode: .inline)
) {
HStack {
VStack(alignment: .leading) {
Text(verbatim: transaction.title)
.lineLimit(1)
.font(.headline)
if let description = transaction.description?.trimmingCharacters(in: CharacterSet([" "])), !description.isEmpty {
Text(verbatim: description)
.lineLimit(1)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.trailing)
}
}
Spacer()
VStack(alignment: .trailing) {
Text(verbatim: transaction.amount.toCurrencyString())
.foregroundColor(transaction.expense ? .red : .green)
.multilineTextAlignment(.trailing)
}
.padding(.leading)
}.padding(5.0)
}
}
}
struct RecurringTransactionsListItemView_Previews: PreviewProvider {
static var previews: some View {
RecurringTransactionsListItemView(MockRecurringTransactionRepository.transaction)
}
}

View file

@ -0,0 +1,56 @@
//
// RecurringTransactionsRepository.swift
// Twigs
//
// Created by William Brawner on 12/6/21.
// Copyright © 2021 William Brawner. All rights reserved.
//
import Foundation
import Combine
protocol RecurringTransactionsRepository {
func getRecurringTransactions(budgetId: String) -> AnyPublisher<[RecurringTransaction], NetworkError>
func getRecurringTransaction(_ id: String) -> AnyPublisher<RecurringTransaction, NetworkError>
func createRecurringTransaction(_ transaction: RecurringTransaction) -> AnyPublisher<RecurringTransaction, NetworkError>
func updateRecurringTransaction(_ transaction: RecurringTransaction) -> AnyPublisher<RecurringTransaction, NetworkError>
func deleteRecurringTransaction(_ id: String) -> AnyPublisher<Empty, NetworkError>
}
#if DEBUG
class MockRecurringTransactionRepository: RecurringTransactionsRepository {
static let transaction: RecurringTransaction = RecurringTransaction(
id: "2",
title: "Test Transaction",
description: "A mock transaction used for testing",
frequency: Frequency(unit: .daily, count: 1, time: Time(from: "09:00:00")!)!,
start: Date(),
end: nil,
amount: 10000,
categoryId: MockCategoryRepository.category.id,
expense: true,
createdBy: MockUserRepository.user.id,
budgetId: MockBudgetRepository.budget.id
)
func getRecurringTransactions(budgetId: String) -> AnyPublisher<[RecurringTransaction], NetworkError> {
return Result.Publisher([MockRecurringTransactionRepository.transaction]).eraseToAnyPublisher()
}
func getRecurringTransaction(_ id: String) -> AnyPublisher<RecurringTransaction, NetworkError> {
return Result.Publisher(MockRecurringTransactionRepository.transaction).eraseToAnyPublisher()
}
func createRecurringTransaction(_ transaction: RecurringTransaction) -> AnyPublisher<RecurringTransaction, NetworkError> {
return Result.Publisher(MockRecurringTransactionRepository.transaction).eraseToAnyPublisher()
}
func updateRecurringTransaction(_ transaction: RecurringTransaction) -> AnyPublisher<RecurringTransaction, NetworkError> {
return Result.Publisher(MockRecurringTransactionRepository.transaction).eraseToAnyPublisher()
}
func deleteRecurringTransaction(_ id: String) -> AnyPublisher<Empty, NetworkError> {
return Result.Publisher(.success(Empty())).eraseToAnyPublisher()
}
}
#endif

View file

@ -12,6 +12,7 @@ struct TabbedBudgetView: View {
@EnvironmentObject var authenticationDataStore: AuthenticationDataStore
@EnvironmentObject var budgetDataStore: BudgetsDataStore
@EnvironmentObject var categoryDataStore: CategoryDataStore
let apiService: TwigsApiService
@State var isSelectingBudget = true
@State var hasSelectedBudget = false
@State var isAddingTransaction = false
@ -65,12 +66,12 @@ struct TabbedBudgetView: View {
.tag(2)
.keyboardShortcut("3")
NavigationView {
ProfileView()
.navigationBarTitle("profile")
RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(apiService, budgetId: budget.id))
.navigationBarTitle("recurring_transactions")
}
.tabItem {
Image(systemName: "person.circle.fill")
Text("profile")
Image(systemName: "arrow.triangle.2.circlepath.circle.fill")
Text("recurring")
}
.tag(3)
.keyboardShortcut("4")

View file

@ -73,3 +73,7 @@
"change_password" = "Change Password";
"change_email" = "Change Email";
"delete_account" = "Delete Account";
// MARK: Recurring Transactions
"recurring" = "Recurring";
"recurring_transactions" = "Recurring Transactions";

View file

@ -74,3 +74,7 @@
"change_password" = "Cambiar Contraseña";
"change_email" = "Cambiar Correo";
"delete_account" = "Eliminar Cuenta";
// MARK: Recurring Transactions
"recurring" = "Recurrentes";
"recurring_transactions" = "Transacciones Recurrentes";