From 0995e7d1a43fd0c7ba607f37348ae4e61d7e48ab Mon Sep 17 00:00:00 2001 From: Amisha Date: Wed, 27 Nov 2024 20:04:14 +0530 Subject: [PATCH] Add expense note --- Data/Data/Model/Expense.swift | 7 +- Data/Data/Repository/ExpenseRepository.swift | 7 +- Splito.xcodeproj/project.pbxproj | 16 ++++ Splito/Localization/Localizable.xcstrings | 15 ++++ .../NoteIcon.imageset/Contents.json | 22 ++++++ .../note-1_svgrepo.com (1).svg | 3 + .../NoteIcon.imageset/note-1_svgrepo.com.svg | 3 + Splito/UI/Home/Expense/AddExpenseView.swift | 41 ++++++++-- .../UI/Home/Expense/AddExpenseViewModel.swift | 29 ++++--- .../Expense Detail/ExpenseDetailsView.swift | 53 ++++++++++++- .../ExpenseDetailsViewModel.swift | 12 ++- .../Expense/Notes/ExpenseAddNoteView.swift | 78 +++++++++++++++++++ .../Notes/ExpenseAddNoteViewModel.swift | 66 ++++++++++++++++ 13 files changed, 326 insertions(+), 26 deletions(-) create mode 100644 Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/Contents.json create mode 100644 Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/note-1_svgrepo.com (1).svg create mode 100644 Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/note-1_svgrepo.com.svg create mode 100644 Splito/UI/Home/Expense/Notes/ExpenseAddNoteView.swift create mode 100644 Splito/UI/Home/Expense/Notes/ExpenseAddNoteViewModel.swift diff --git a/Data/Data/Model/Expense.swift b/Data/Data/Model/Expense.swift index 4a2da513b..6fda424f7 100644 --- a/Data/Data/Model/Expense.swift +++ b/Data/Data/Model/Expense.swift @@ -17,6 +17,7 @@ public struct Expense: Codable, Hashable, Identifiable { public var paidBy: [String: Double] public let addedBy: String public var updatedBy: String + public var note: String? public var imageUrl: String? public var splitTo: [String] // Reference to user ids involved in the split public var splitType: SplitType @@ -24,14 +25,15 @@ public struct Expense: Codable, Hashable, Identifiable { public var isActive: Bool public init(name: String, amount: Double, date: Timestamp, paidBy: [String: Double], addedBy: String, - updatedBy: String, imageUrl: String? = nil, splitTo: [String], splitType: SplitType = .equally, - splitData: [String: Double]? = [:], isActive: Bool = true) { + updatedBy: String, note: String? = nil, imageUrl: String? = nil, splitTo: [String], + splitType: SplitType = .equally, splitData: [String: Double]? = [:], isActive: Bool = true) { self.name = name self.amount = amount self.date = date self.paidBy = paidBy self.addedBy = addedBy self.updatedBy = updatedBy + self.note = note self.imageUrl = imageUrl self.splitTo = splitTo self.splitType = splitType @@ -47,6 +49,7 @@ public struct Expense: Codable, Hashable, Identifiable { case paidBy = "paid_by" case addedBy = "added_by" case updatedBy = "updated_by" + case note = "note" case imageUrl = "image_url" case splitTo = "split_to" case splitType = "split_type" diff --git a/Data/Data/Repository/ExpenseRepository.swift b/Data/Data/Repository/ExpenseRepository.swift index 47a79fa8a..9fb26d62e 100644 --- a/Data/Data/Repository/ExpenseRepository.swift +++ b/Data/Data/Repository/ExpenseRepository.swift @@ -71,9 +71,10 @@ public class ExpenseRepository: ObservableObject { private func hasExpenseChanged(_ expense: Expense, oldExpense: Expense) -> Bool { return oldExpense.name != expense.name || oldExpense.amount != expense.amount || oldExpense.date.dateValue() != expense.date.dateValue() || oldExpense.paidBy != expense.paidBy || - oldExpense.updatedBy != expense.updatedBy || oldExpense.imageUrl != expense.imageUrl || - oldExpense.splitTo != expense.splitTo || oldExpense.splitType != expense.splitType || - oldExpense.splitData != expense.splitData || oldExpense.isActive != expense.isActive + oldExpense.updatedBy != expense.updatedBy || oldExpense.note != expense.note || + oldExpense.imageUrl != expense.imageUrl || oldExpense.splitTo != expense.splitTo || + oldExpense.splitType != expense.splitType || oldExpense.splitData != expense.splitData || + oldExpense.isActive != expense.isActive } public func updateExpense(group: Groups, expense: Expense, oldExpense: Expense, type: ActivityType) async throws { diff --git a/Splito.xcodeproj/project.pbxproj b/Splito.xcodeproj/project.pbxproj index cace810c9..f91343954 100644 --- a/Splito.xcodeproj/project.pbxproj +++ b/Splito.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 0BF8F99614F85846D78DE106 /* Pods_Splito_SplitoUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2B9BBF3F71277A6AA5329CB /* Pods_Splito_SplitoUITests.framework */; }; + 210CC0382CF6DA7A0035682E /* ExpenseAddNoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210CC0372CF6DA7A0035682E /* ExpenseAddNoteView.swift */; }; + 210CC03B2CF6DA920035682E /* ExpenseAddNoteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210CC03A2CF6DA920035682E /* ExpenseAddNoteViewModel.swift */; }; 213BA0602C0F465000116130 /* GroupSettleUpRouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 213BA05F2C0F465000116130 /* GroupSettleUpRouteView.swift */; }; 213BA0662C11B70F00116130 /* HomeRouteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 213BA0652C11B70F00116130 /* HomeRouteViewModel.swift */; }; 214CF8492C2977E10044C188 /* CalculateExpensesFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214CF8482C2977E10044C188 /* CalculateExpensesFunctions.swift */; }; @@ -153,6 +155,8 @@ /* Begin PBXFileReference section */ 038CCD15E82A4E16A4AD213C /* Pods-Splito-SplitoUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Splito-SplitoUITests.release.xcconfig"; path = "Target Support Files/Pods-Splito-SplitoUITests/Pods-Splito-SplitoUITests.release.xcconfig"; sourceTree = ""; }; + 210CC0372CF6DA7A0035682E /* ExpenseAddNoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpenseAddNoteView.swift; sourceTree = ""; }; + 210CC03A2CF6DA920035682E /* ExpenseAddNoteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpenseAddNoteViewModel.swift; sourceTree = ""; }; 213BA05F2C0F465000116130 /* GroupSettleUpRouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSettleUpRouteView.swift; sourceTree = ""; }; 213BA0652C11B70F00116130 /* HomeRouteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeRouteViewModel.swift; sourceTree = ""; }; 214CF8482C2977E10044C188 /* CalculateExpensesFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculateExpensesFunctions.swift; sourceTree = ""; }; @@ -309,6 +313,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 210CC0392CF6DA7F0035682E /* Notes */ = { + isa = PBXGroup; + children = ( + 210CC0372CF6DA7A0035682E /* ExpenseAddNoteView.swift */, + 210CC03A2CF6DA920035682E /* ExpenseAddNoteViewModel.swift */, + ); + path = Notes; + sourceTree = ""; + }; 21559CAA2CBD21850039F127 /* ActivityLog */ = { isa = PBXGroup; children = ( @@ -495,6 +508,7 @@ D85E86F02BB41CBA002EDF76 /* Detail Selection */, D856C7302BCFD2080008A341 /* Expense Detail */, D8CD952A2BD65E6400407B47 /* Expense Split Option */, + 210CC0392CF6DA7F0035682E /* Notes */, ); path = Expense; sourceTree = ""; @@ -1060,6 +1074,7 @@ 2177692D2C20316B009B3B37 /* GroupTransactionDetailViewModel.swift in Sources */, D83344732C0F2D9500CD9F05 /* GroupPaymentView.swift in Sources */, D83344582C0DD06F00CD9F05 /* GroupSettleUpView.swift in Sources */, + 210CC03B2CF6DA920035682E /* ExpenseAddNoteViewModel.swift in Sources */, D8E244BF2B98592C00C6C82A /* GroupHomeViewModel.swift in Sources */, D833446C2C0F2D4200CD9F05 /* GroupWhoGettingPaidView.swift in Sources */, D8D14A562BA189EC00F45FF2 /* JoinMemberView.swift in Sources */, @@ -1089,6 +1104,7 @@ 214CF8492C2977E10044C188 /* CalculateExpensesFunctions.swift in Sources */, 213BA0602C0F465000116130 /* GroupSettleUpRouteView.swift in Sources */, D8AC26F72B84B12800CEAAD3 /* LoginView.swift in Sources */, + 210CC0382CF6DA7A0035682E /* ExpenseAddNoteView.swift in Sources */, D8CD952E2BD65F4500407B47 /* ExpenseSplitOptionsViewModel.swift in Sources */, D85E86E32BAB06D9002EDF76 /* AddExpenseViewModel.swift in Sources */, D8A7CA702BA484370014EC67 /* GroupSettingView.swift in Sources */, diff --git a/Splito/Localization/Localizable.xcstrings b/Splito/Localization/Localizable.xcstrings index 2c2b5b38f..b86bea189 100644 --- a/Splito/Localization/Localizable.xcstrings +++ b/Splito/Localization/Localizable.xcstrings @@ -156,6 +156,9 @@ }, "Add expense" : { + }, + "Add note" : { + }, "added" : { "extractionState" : "manual" @@ -217,6 +220,9 @@ }, "Are you sure you want to sign out?" : { "extractionState" : "manual" + }, + "Attachment:" : { + }, "Authentication failed" : { "extractionState" : "manual" @@ -331,6 +337,9 @@ }, "Enter your last name" : { "extractionState" : "manual" + }, + "Enter your note here..." : { + }, "Enter your phone number" : { "extractionState" : "manual" @@ -353,6 +362,9 @@ "Expense deleted successfully" : { "extractionState" : "manual" }, + "Failed to save note." : { + "extractionState" : "manual" + }, "First Name" : { "extractionState" : "manual" }, @@ -502,6 +514,9 @@ }, "not involved" : { + }, + "Note:" : { + }, "Ok" : { "extractionState" : "manual" diff --git a/Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/Contents.json b/Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/Contents.json new file mode 100644 index 000000000..db36d9815 --- /dev/null +++ b/Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "note-1_svgrepo.com.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "note-1_svgrepo.com (1).svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/note-1_svgrepo.com (1).svg b/Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/note-1_svgrepo.com (1).svg new file mode 100644 index 000000000..a4c00740a --- /dev/null +++ b/Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/note-1_svgrepo.com (1).svg @@ -0,0 +1,3 @@ + + + diff --git a/Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/note-1_svgrepo.com.svg b/Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/note-1_svgrepo.com.svg new file mode 100644 index 000000000..59bd5cf7f --- /dev/null +++ b/Splito/Resource/Assets.xcassets/Images/Group/Add Expense/NoteIcon.imageset/note-1_svgrepo.com.svg @@ -0,0 +1,3 @@ + + + diff --git a/Splito/UI/Home/Expense/AddExpenseView.swift b/Splito/UI/Home/Expense/AddExpenseView.swift index 039c47f21..228eefdd1 100644 --- a/Splito/UI/Home/Expense/AddExpenseView.swift +++ b/Splito/UI/Home/Expense/AddExpenseView.swift @@ -35,9 +35,9 @@ struct AddExpenseView: View { .scrollBounceBehavior(.basedOnSize) AddExpenseFooterView(date: $viewModel.expenseDate, showImagePickerOptions: $viewModel.showImagePickerOptions, - expenseImage: viewModel.expenseImage, expenseImageUrl: viewModel.expenseImageUrl, - handleExpenseImageTap: viewModel.handleExpenseImageTap, - handleActionSelection: viewModel.handleActionSelection(_:)) + expenseImage: viewModel.expenseImage, expenseImageUrl: viewModel.expenseImageUrl, + handleNoteBtnTap: viewModel.handleNoteBtnTap, handleExpenseImageTap: viewModel.handleExpenseImageTap, + handleActionSelection: viewModel.handleActionSelection(_:)) } } .background(surfaceColor) @@ -74,6 +74,13 @@ struct AddExpenseView: View { ) } } + .sheet(isPresented: $viewModel.showAddNoteEditor) { + NavigationStack { + ExpenseAddNoteView(viewModel: ExpenseAddNoteViewModel(group: viewModel.selectedGroup, expense: viewModel.expense, + expenseNote: viewModel.expenseNote, + handleSaveNoteTap: viewModel.handleNoteSaveBtnTap(note:))) + } + } .sheet(isPresented: $viewModel.showImagePicker) { ImagePickerView(cropOption: .square, sourceType: !viewModel.sourceTypeIsCamera ? .photoLibrary : .camera, @@ -81,10 +88,7 @@ struct AddExpenseView: View { } .toolbar { ToolbarItem(placement: .topBarLeading) { - Button("Cancel") { - dismiss() - } - .foregroundStyle(.blue) + CancelButton() } ToolbarItem(placement: .topBarTrailing) { CheckmarkButton(showLoader: viewModel.showLoader) { @@ -208,6 +212,7 @@ private struct AddExpenseFooterView: View { let expenseImage: UIImage? let expenseImageUrl: String? + let handleNoteBtnTap: (() -> Void) let handleExpenseImageTap: (() -> Void) let handleActionSelection: ((ActionsOfSheet) -> Void) @@ -222,6 +227,8 @@ private struct AddExpenseFooterView: View { DatePickerView(date: $date, isForAddExpense: true) ExpenseImagePickerView(image: expenseImage, imageUrl: expenseImageUrl, handleImageBtnTap: handleExpenseImageTap) + + NoteButtonView(handleNoteBtnTap: handleNoteBtnTap) } .padding(.vertical, 12) .padding(.horizontal, 16) @@ -265,3 +272,23 @@ private struct ExpenseImagePickerView: View { } } } + +private struct NoteButtonView: View { + + let handleNoteBtnTap: (() -> Void) + + var body: some View { + Button { + UIApplication.shared.endEditing() + handleNoteBtnTap() + } label: { + Image(.noteIcon) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + .padding(4) + .background(container2Color) + .cornerRadius(8) + } + } +} diff --git a/Splito/UI/Home/Expense/AddExpenseViewModel.swift b/Splito/UI/Home/Expense/AddExpenseViewModel.swift index f9ffddd8b..63c02fe58 100644 --- a/Splito/UI/Home/Expense/AddExpenseViewModel.swift +++ b/Splito/UI/Home/Expense/AddExpenseViewModel.swift @@ -19,6 +19,7 @@ class AddExpenseViewModel: BaseViewModel, ObservableObject { @Inject private var expenseRepository: ExpenseRepository @Published var expenseName = "" + @Published var expenseNote: String = "" @Published private(set) var expenseImageUrl: String? @Published private(set) var payerName = "You" @@ -27,6 +28,7 @@ class AddExpenseViewModel: BaseViewModel, ObservableObject { @Published var expenseAmount: Double = 0 @Published var showImagePicker = false + @Published var showAddNoteEditor = false @Published var showGroupSelection = false @Published var showPayerSelection = false @Published var showImagePickerOptions = false @@ -50,15 +52,14 @@ class AddExpenseViewModel: BaseViewModel, ObservableObject { } } - var expenseId: String? - private var groupId: String? + let expenseId: String? + private let groupId: String? private let router: Router init(router: Router, groupId: String? = nil, expenseId: String? = nil) { self.router = router self.groupId = groupId self.expenseId = expenseId - self.groupId = groupId super.init() @@ -72,7 +73,7 @@ class AddExpenseViewModel: BaseViewModel, ObservableObject { } } - // MARK: - Data Loading +// MARK: - Data Loading private func fetchGroup(groupId: String) async { do { viewState = .loading @@ -134,6 +135,7 @@ class AddExpenseViewModel: BaseViewModel, ObservableObject { splitType = expense.splitType selectedPayers = expense.paidBy expenseImageUrl = expense.imageUrl + expenseNote = expense.note ?? "" if let splitData = expense.splitData { self.splitData = splitData @@ -177,7 +179,6 @@ class AddExpenseViewModel: BaseViewModel, ObservableObject { // MARK: - User Actions extension AddExpenseViewModel { - private func updatePayerName() { Task { if selectedPayers.count == 1 { @@ -213,6 +214,15 @@ extension AddExpenseViewModel { showImagePickerOptions = true } + func handleNoteBtnTap() { + showAddNoteEditor = true + } + + func handleNoteSaveBtnTap(note: String) { + showAddNoteEditor = false + self.expenseNote = note + } + func handleActionSelection(_ action: ActionsOfSheet) { switch action { case .camera: @@ -365,7 +375,7 @@ extension AddExpenseViewModel { private func handleAddExpenseAction(userId: String, group: Groups) async -> Bool { let expense = Expense(name: expenseName.trimming(spaces: .leadingAndTrailing), amount: expenseAmount, date: Timestamp(date: expenseDate), paidBy: selectedPayers, addedBy: userId, updatedBy: userId, - splitTo: (splitType == .equally) ? selectedMembers : splitData.map({ $0.key }), + note: expenseNote, splitTo: (splitType == .equally) ? selectedMembers : splitData.map({ $0.key }), splitType: splitType, splitData: splitData) return await addExpense(group: group, expense: expense) @@ -405,6 +415,7 @@ extension AddExpenseViewModel { newExpense.amount = expenseAmount newExpense.date = Timestamp(date: expenseDate) newExpense.updatedBy = userId + newExpense.note = expenseNote if selectedPayers.count == 1, let payerId = selectedPayers.keys.first { newExpense.paidBy = [payerId: expenseAmount] @@ -451,9 +462,9 @@ extension AddExpenseViewModel { private func hasExpenseChanged(_ expense: Expense, oldExpense: Expense) -> Bool { return oldExpense.name != expense.name || oldExpense.amount != expense.amount || oldExpense.date.dateValue() != expense.date.dateValue() || oldExpense.paidBy != expense.paidBy || - oldExpense.imageUrl != expense.imageUrl || oldExpense.splitTo != expense.splitTo || - oldExpense.splitType != expense.splitType || oldExpense.splitData != expense.splitData || - oldExpense.isActive != expense.isActive + oldExpense.note != expense.note || oldExpense.imageUrl != expense.imageUrl || + oldExpense.splitTo != expense.splitTo || oldExpense.splitType != expense.splitType || + oldExpense.splitData != expense.splitData || oldExpense.isActive != expense.isActive } private func updateGroupMemberBalance(expense: Expense, updateType: ExpenseUpdateType) async { diff --git a/Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift b/Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift index be4626c90..5d93920e7 100644 --- a/Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift +++ b/Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift @@ -22,15 +22,27 @@ struct ExpenseDetailsView: View { LoaderView() } else { ScrollView { - VStack(alignment: .leading, spacing: 32) { + VStack(alignment: .leading, spacing: 16) { ExpenseHeaderView(viewModel: viewModel) ExpenseInfoView(viewModel: viewModel) if let imageUrl = viewModel.expense?.imageUrl { - ExpenseImageView(showImageDisplayView: $showImageDisplayView, imageUrl: imageUrl) - .aspectRatio(16/9, contentMode: .fit) - .cornerRadius(12) + VStack(spacing: 8) { + Text("Attachment:") + .font(.subTitle3()) + .foregroundStyle(disableText) + .frame(maxWidth: .infinity, alignment: .leading) + + ExpenseImageView(showImageDisplayView: $showImageDisplayView, imageUrl: imageUrl) + .frame(height: 140) + .frame(maxWidth: .infinity) + .cornerRadius(12) + } + } + + if let note = viewModel.expense?.note, !note.isEmpty { + ExpenseNoteView(note: note, handleNoteTap: viewModel.handleNoteTap) } VSpacer(24) @@ -51,6 +63,12 @@ struct ExpenseDetailsView: View { AddExpenseView(viewModel: AddExpenseViewModel(router: viewModel.router, groupId: viewModel.groupId, expenseId: viewModel.expenseId)) } } + .fullScreenCover(isPresented: $viewModel.showAddNoteEditor) { + NavigationStack { + ExpenseAddNoteView(viewModel: ExpenseAddNoteViewModel(group: viewModel.group, expense: viewModel.expense, + expenseNote: viewModel.expenseNote)) + } + } .toolbarRole(.editor) .toolbar { ToolbarItem(placement: .topBarLeading) { @@ -185,5 +203,32 @@ private struct ExpenseInfoView: View { .font(.body3()) .foregroundStyle(disableText) } + .padding(.top, 8) + } +} + +private struct ExpenseNoteView: View { + + let note: String + + let handleNoteTap: (() -> Void) + + var body: some View { + VStack(spacing: 8) { + Text("Note:") + .font(.subTitle3()) + .foregroundStyle(disableText) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(note) + .font(.subTitle2()) + .foregroundStyle(primaryText) + .lineSpacing(3) + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(containerColor) + .cornerRadius(12) + .onTapGestureForced(perform: handleNoteTap) + } } } diff --git a/Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsViewModel.swift b/Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsViewModel.swift index d8c5fc5a2..4ea0093f9 100644 --- a/Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsViewModel.swift +++ b/Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsViewModel.swift @@ -20,7 +20,10 @@ class ExpenseDetailsViewModel: BaseViewModel, ObservableObject { @Published private(set) var expenseUsersData: [AppUser] = [] @Published private(set) var viewState: ViewState = .loading + @Published var expenseNote: String = "" @Published private(set) var groupImageUrl: String = "" + + @Published var showAddNoteEditor = false @Published var showEditExpenseSheet = false var group: Groups? @@ -90,6 +93,7 @@ class ExpenseDetailsViewModel: BaseViewModel, ObservableObject { } self.expense = expense + self.expenseNote = expense.note ?? "" self.expenseUsersData = userData } @@ -107,10 +111,16 @@ class ExpenseDetailsViewModel: BaseViewModel, ObservableObject { } // MARK: - User Actions - func getMemberDataBy(id: String) -> AppUser? { + func getMemberDataBy(id: String) -> AppUser? { return expenseUsersData.first(where: { $0.id == id }) } + func handleNoteTap() { + guard let expense, expense.isActive, let userId = preference.user?.id, + let group, group.members.contains(userId) else { return } + showAddNoteEditor = true + } + func handleEditBtnAction() { guard validateUserPermission(operationText: "edited", action: "edit"), validateGroupMembers(action: "edited") else { return } diff --git a/Splito/UI/Home/Expense/Notes/ExpenseAddNoteView.swift b/Splito/UI/Home/Expense/Notes/ExpenseAddNoteView.swift new file mode 100644 index 000000000..20d0d278a --- /dev/null +++ b/Splito/UI/Home/Expense/Notes/ExpenseAddNoteView.swift @@ -0,0 +1,78 @@ +// +// ExpenseAddNoteView.swift +// Splito +// +// Created by Nirali Sonani on 27/11/24. +// + +import SwiftUI +import BaseStyle + +struct ExpenseAddNoteView: View { + @Environment(\.dismiss) var dismiss + + @StateObject var viewModel: ExpenseAddNoteViewModel + + @State private var tempNote: String = "" + @FocusState private var isFocused: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + TextField("Enter your note here...", text: $tempNote, axis: .vertical) + .font(.subTitle2()) + .foregroundStyle(primaryText) + .focused($isFocused) + .tint(primaryColor) + .autocorrectionDisabled() + .padding(.horizontal, 16) + .padding(.vertical, 12) + .overlay { + RoundedRectangle(cornerRadius: 12) + .stroke(outlineColor, lineWidth: 1) + } + + Spacer() + } + .padding(16) + .frame(maxWidth: isIpad ? 600 : nil, alignment: .center) + .frame(maxWidth: .infinity, alignment: .center) + .background(surfaceColor) + .navigationTitle("Add note") + .navigationBarTitleDisplayMode(.inline) + .toastView(toast: $viewModel.toast) + .backport.alert(isPresented: $viewModel.showAlert, alertStruct: viewModel.alert) + .onAppear { + tempNote = viewModel.expenseNote + isFocused = true + } + .toolbar { + ToolbarItem(placement: .topBarLeading) { + CancelButton() + } + ToolbarItem(placement: .topBarTrailing) { + CheckmarkButton(showLoader: viewModel.showLoader) { + viewModel.expenseNote = tempNote.trimming(spaces: .leadingAndTrailing) + Task { + let isActionSucceed = await viewModel.handleSaveNoteAction() + if isActionSucceed { + dismiss() + } else { + viewModel.showSaveFailedError() + } + } + } + } + } + } +} + +struct CancelButton: View { + @Environment(\.dismiss) var dismiss + + var body: some View { + Button("Cancel") { + dismiss() + } + .foregroundStyle(.blue) + } +} diff --git a/Splito/UI/Home/Expense/Notes/ExpenseAddNoteViewModel.swift b/Splito/UI/Home/Expense/Notes/ExpenseAddNoteViewModel.swift new file mode 100644 index 000000000..894de17fd --- /dev/null +++ b/Splito/UI/Home/Expense/Notes/ExpenseAddNoteViewModel.swift @@ -0,0 +1,66 @@ +// +// ExpenseAddNoteViewModel.swift +// Splito +// +// Created by Nirali Sonani on 27/11/24. +// + +import Data +import BaseStyle +import Foundation + +class ExpenseAddNoteViewModel: BaseViewModel, ObservableObject { + + @Inject private var expenseRepository: ExpenseRepository + + @Published var expenseNote: String + @Published private(set) var showLoader: Bool = false + + private let group: Groups? + private let expense: Expense? + private let handleSaveNoteTap: ((String) -> Void)? + + init(group: Groups?, expense: Expense?, expenseNote: String, handleSaveNoteTap: ((String) -> Void)? = nil) { + self.group = group + self.expense = expense + self.expenseNote = expenseNote + self.handleSaveNoteTap = handleSaveNoteTap + super.init() + } + + // MARK: - User Actions + func showSaveFailedError() { + self.showToastFor(toast: ToastPrompt(type: .error, title: "Oops", message: "Failed to save note.")) + } + + func handleSaveNoteAction() async -> Bool { + if let handleSaveNoteTap { + handleSaveNoteTap(expenseNote) + return true + } + + guard let expense, expense.note != expenseNote else { return true } + return await updateExpenseNote() + } + + private func updateExpenseNote() async -> Bool { + guard let group, let expense else { return false } + + do { + showLoader = true + var updatedExpense = expense + updatedExpense.note = expenseNote + + try await expenseRepository.updateExpense(group: group, expense: updatedExpense, oldExpense: expense, type: .expenseUpdated) + NotificationCenter.default.post(name: .updateExpense, object: updatedExpense) + + showLoader = false + LogD("ExpenseAddNoteViewModel: \(#function) Expense note updated successfully.") + return true + } catch { + LogE("ExpenseAddNoteViewModel: \(#function) Failed to update expense note: \(error).") + showToastForError() + return false + } + } +}