Skip to content

Commit 8b5bd72

Browse files
committed
Added groupCollection query & fixed ui
1 parent 90f0b82 commit 8b5bd72

File tree

5 files changed

+100
-92
lines changed

5 files changed

+100
-92
lines changed

Data/Data/Repository/ExpenseRepository.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ public class ExpenseRepository: ObservableObject {
165165
return try await store.fetchExpenseBy(groupId: groupId, expenseId: expenseId)
166166
}
167167

168-
public func fetchAllUserExpenses(userId: String, limit: Int = 10) async throws -> [Expense] {
169-
return try await store.fetchAllUserExpenses(userId: userId, limit: limit)
168+
public func fetchExpensesForUser(userId: String, limit: Int = 10, lastDocument: DocumentSnapshot? = nil) async throws -> (expenses: [Expense], lastDocument: DocumentSnapshot?) {
169+
return try await store.fetchExpensesForUser(userId: userId, limit: limit, lastDocument: lastDocument)
170170
}
171171
}

Data/Data/Store/ExpenseStore.swift

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ public class ExpenseStore: ObservableObject {
1414
private let COLLECTION_NAME: String = "groups"
1515
private let SUB_COLLECTION_NAME: String = "expenses"
1616

17-
private var groupReference: CollectionReference {
18-
database.collection(COLLECTION_NAME)
19-
}
20-
2117
private func expenseReference(groupId: String) -> CollectionReference {
2218
database
2319
.collection(COLLECTION_NAME)
@@ -64,43 +60,53 @@ public class ExpenseStore: ObservableObject {
6460
return (expenses, snapshot.documents.last)
6561
}
6662

67-
func fetchAllUserExpenses(userId: String, limit: Int) async throws -> [Expense] {
68-
var allExpenses: [Expense] = []
69-
var lastGroupDocument: DocumentSnapshot?
70-
71-
repeat {
72-
let (groups, lastDoc) = try await fetchGroupsBy(userId: userId, limit: limit, lastDocument: lastGroupDocument)
73-
lastGroupDocument = lastDoc
74-
75-
for group in groups {
76-
var lastExpenseDocument: DocumentSnapshot?
77-
repeat {
78-
let (expenses, lastDoc) = try await fetchExpensesBy(groupId: group.id ?? "", limit: limit, lastDocument: lastExpenseDocument)
79-
lastExpenseDocument = lastDoc
80-
allExpenses.append(contentsOf: expenses)
81-
} while lastExpenseDocument != nil
82-
}
83-
} while lastGroupDocument != nil
84-
85-
return allExpenses
86-
}
63+
func fetchExpensesForUser(userId: String, limit: Int, lastDocument: DocumentSnapshot?) async throws -> (expenses: [Expense], lastDocument: DocumentSnapshot?) {
64+
var userExpenses: [Expense] = []
8765

88-
func fetchGroupsBy(userId: String, limit: Int, lastDocument: DocumentSnapshot?) async throws -> (data: [Groups], lastDocument: DocumentSnapshot?) {
89-
var query = groupReference
66+
// Step 1: Fetch groups where the user is a member
67+
let groupSnapshot = try await database.collection("groups")
9068
.whereField("is_active", isEqualTo: true)
9169
.whereField("members", arrayContains: userId)
92-
.order(by: "updated_at", descending: true)
93-
.limit(to: limit)
70+
.getDocuments()
9471

95-
if let lastDocument {
96-
query = query.start(afterDocument: lastDocument)
97-
}
72+
let groupDocuments = groupSnapshot.documents
73+
var lastFetchedDocument: DocumentSnapshot?
9874

99-
let snapshot = try await query.getDocuments()
100-
let groups = try snapshot.documents.compactMap { document in
101-
try document.data(as: Groups.self)
75+
// Step 2: Fetch expenses from each group with pagination
76+
for groupDoc in groupDocuments {
77+
let groupId = groupDoc.documentID
78+
79+
var query = database.collection("groups")
80+
.document(groupId)
81+
.collection("expenses")
82+
.whereField("is_active", isEqualTo: true)
83+
.order(by: "date", descending: true)
84+
.limit(to: limit)
85+
86+
// Apply pagination
87+
if let lastDocument = lastDocument {
88+
query = query.start(afterDocument: lastDocument)
89+
}
90+
91+
let expenseSnapshot = try await query.getDocuments()
92+
93+
let fetchedExpenses = expenseSnapshot.documents.compactMap { doc -> Expense? in
94+
do {
95+
return try doc.data(as: Expense.self)
96+
} catch {
97+
LogE("ExpenseStore: \(#function) Error decoding expense: \(error.localizedDescription)")
98+
return nil
99+
}
100+
}
101+
102+
userExpenses.append(contentsOf: fetchedExpenses)
103+
104+
// Track the last document for pagination
105+
if let lastDoc = expenseSnapshot.documents.last {
106+
lastFetchedDocument = lastDoc
107+
}
102108
}
103109

104-
return (groups, snapshot.documents.last)
110+
return (userExpenses, lastFetchedDocument)
105111
}
106112
}

Splito/UI/Home/ActivityLog/Search/SearchExpensesView.swift

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,17 @@ struct SearchExpensesView: View {
1616
@FocusState private var isFocused: Bool
1717

1818
var body: some View {
19-
VStack(alignment: .center, spacing: 0) {
20-
if .noInternet == viewModel.viewState || .somethingWentWrong == viewModel.viewState {
21-
ErrorView(isForNoInternet: viewModel.viewState == .noInternet, onClick: {
22-
viewModel.fetchInitialExpenses()
23-
})
24-
} else if case .loading = viewModel.viewState {
25-
LoaderView()
26-
Spacer(minLength: 60)
27-
} else {
28-
if case .noExpense = viewModel.viewState {
29-
30-
} else if case .hasExpense = viewModel.viewState {
31-
VSpacer(4)
32-
33-
SearchBar(text: $viewModel.searchedExpense, isFocused: $isFocused, placeholder: "Search groups")
19+
GeometryReader { geometry in
20+
VStack(alignment: .center, spacing: 0) {
21+
if .noInternet == viewModel.viewState || .somethingWentWrong == viewModel.viewState {
22+
ErrorView(isForNoInternet: viewModel.viewState == .noInternet, onClick: {
23+
viewModel.fetchInitialExpenses()
24+
})
25+
} else if case .loading = viewModel.viewState {
26+
LoaderView()
27+
Spacer(minLength: 60)
28+
} else {
29+
SearchBar(text: $viewModel.searchedExpense, isFocused: $isFocused, placeholder: "Search...")
3430
.padding(.vertical, -7)
3531
.padding(.horizontal, 3)
3632
.overlay(content: {
@@ -41,15 +37,19 @@ struct SearchExpensesView: View {
4137
.task {
4238
isFocused = true
4339
}
44-
.padding([.horizontal, .top], 16)
45-
.padding(.bottom, 8)
40+
.padding(.horizontal, 16)
4641

47-
ExpenseListView(viewModel: viewModel)
42+
if case .noExpense = viewModel.viewState {
43+
EmptyStateView(geometry: geometry)
44+
} else if case .hasExpense = viewModel.viewState {
45+
ExpenseListView(viewModel: viewModel, geometry: geometry)
46+
}
4847
}
4948
}
5049
}
5150
.background(surfaceColor)
5251
.toastView(toast: $viewModel.toast)
52+
.toolbarRole(.editor)
5353
.alertView.alert(isPresented: $viewModel.showAlert, alertStruct: viewModel.alert)
5454
}
5555
}
@@ -58,40 +58,40 @@ private struct ExpenseListView: View {
5858

5959
@ObservedObject var viewModel: SearchExpensesViewModel
6060

61+
let geometry: GeometryProxy
62+
6163
var body: some View {
62-
GeometryReader { geometry in
63-
List {
64-
Group {
65-
if !viewModel.groupExpenses.isEmpty {
66-
ForEach(viewModel.groupExpenses.keys.sorted(by: sortMonthYearStrings), id: \.self) { month in
67-
Section(header: sectionHeader(month: month)) {
68-
ForEach(viewModel.groupExpenses[month] ?? [], id: \.expense.id) { expense in
69-
GroupExpenseItemView(expenseWithUser: expense,
70-
isLastItem: expense.expense == (viewModel.groupExpenses[month] ?? []).last?.expense)
71-
.onTouchGesture {
72-
viewModel.handleExpenseItemTap(expenseId: expense.expense.id ?? "")
73-
}
74-
.id(expense.expense.id)
64+
List {
65+
Group {
66+
if !viewModel.groupExpenses.isEmpty {
67+
ForEach(viewModel.groupExpenses.keys.sorted(by: sortMonthYearStrings), id: \.self) { month in
68+
Section(header: sectionHeader(month: month)) {
69+
ForEach(viewModel.groupExpenses[month] ?? [], id: \.expense.id) { expense in
70+
GroupExpenseItemView(expenseWithUser: expense,
71+
isLastItem: expense.expense == (viewModel.groupExpenses[month] ?? []).last?.expense)
72+
.onTouchGesture {
73+
viewModel.handleExpenseItemTap(expenseId: expense.expense.id ?? "")
7574
}
75+
.id(expense.expense.id)
7676
}
7777
}
78+
}
7879

79-
if viewModel.hasMoreExpenses {
80-
ProgressView()
81-
.frame(maxWidth: .infinity, alignment: .center)
82-
.onAppear(perform: viewModel.loadMoreExpenses)
83-
.padding(.vertical, 8)
84-
}
85-
} else if viewModel.groupExpenses.isEmpty {
86-
ExpenseNotFoundView(geometry: geometry, searchedExpense: viewModel.searchedExpense)
80+
if viewModel.hasMoreExpenses {
81+
ProgressView()
82+
.frame(maxWidth: .infinity, alignment: .center)
83+
.onAppear(perform: viewModel.loadMoreExpenses)
84+
.padding(.vertical, 8)
8785
}
86+
} else if viewModel.groupExpenses.isEmpty {
87+
ExpenseNotFoundView(minHeight: geometry.size.height - 90, searchedExpense: viewModel.searchedExpense)
8888
}
89-
.listRowSeparator(.hidden)
90-
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
91-
.listRowBackground(surfaceColor)
9289
}
93-
.listStyle(.plain)
90+
.listRowSeparator(.hidden)
91+
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
92+
.listRowBackground(surfaceColor)
9493
}
94+
.listStyle(.plain)
9595
}
9696

9797
private func sectionHeader(month: String) -> some View {

Splito/UI/Home/ActivityLog/Search/SearchExpensesViewModel.swift

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,29 +43,31 @@ class SearchExpensesViewModel: BaseViewModel, ObservableObject {
4343
fetchInitialExpenses()
4444
}
4545

46-
func fetchInitialExpenses(needToReload: Bool = false) {
46+
func fetchInitialExpenses() {
4747
lastDocument = nil
4848
Task {
49-
await fetchAllUserExpenses(needToReload: needToReload)
49+
await fetchAllUserExpenses()
5050
}
5151
}
5252

5353
// MARK: - Data Loading
54-
private func fetchAllUserExpenses(needToReload: Bool = false) async {
55-
guard let userId = preference.user?.id, hasMoreExpenses || needToReload else {
54+
private func fetchAllUserExpenses() async {
55+
guard let userId = preference.user?.id, hasMoreExpenses else {
5656
viewState = .noExpense
5757
return
5858
}
59+
5960
if lastDocument == nil {
6061
expensesWithUser = []
6162
}
6263

6364
do {
64-
let result = try await expenseRepository.fetchAllUserExpenses(userId: userId, limit: EXPENSES_LIMIT)
65-
expenses = lastDocument == nil ? result.uniqued() : (expenses + result.uniqued())
66-
67-
await combineMemberWithExpense(expenses: result.uniqued())
68-
hasMoreExpenses = !(result.count < self.EXPENSES_LIMIT)
65+
let result = try await expenseRepository.fetchExpensesForUser(userId: userId, limit: EXPENSES_LIMIT, lastDocument: lastDocument)
66+
self.expenses = lastDocument == nil ? result.expenses.uniqued() : (expenses + result.expenses.uniqued())
67+
lastDocument = result.lastDocument
68+
69+
await combineMemberWithExpense(expenses: result.expenses.uniqued())
70+
hasMoreExpenses = !(result.expenses.count < self.EXPENSES_LIMIT)
6971
LogD("SearchExpensesViewModel: \(#function) Expenses fetched successfully.")
7072
} catch {
7173
LogE("SearchExpensesViewModel: \(#function) Failed to fetch expenses: \(error).")

Splito/UI/Home/Groups/Group/GroupExpenseListView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ struct GroupExpenseListView: View {
9191
.padding(.vertical, 8)
9292
}
9393
} else if viewModel.groupExpenses.isEmpty && viewModel.showSearchBar {
94-
ExpenseNotFoundView(geometry: geometry, searchedExpense: viewModel.searchedExpense)
94+
ExpenseNotFoundView(minHeight: geometry.size.height - 280, searchedExpense: viewModel.searchedExpense)
9595
}
9696
}
9797
.listRowSeparator(.hidden)
@@ -369,7 +369,7 @@ private struct GroupExpenseMemberOweView: View {
369369

370370
struct ExpenseNotFoundView: View {
371371

372-
let geometry: GeometryProxy
372+
let minHeight: CGFloat
373373
let searchedExpense: String
374374

375375
var body: some View {
@@ -387,7 +387,7 @@ struct ExpenseNotFoundView: View {
387387
.multilineTextAlignment(.center)
388388
.padding(.horizontal, 16)
389389
.frame(maxWidth: .infinity, alignment: .center)
390-
.frame(minHeight: geometry.size.height - 280, maxHeight: .infinity, alignment: .center)
390+
.frame(minHeight: minHeight, maxHeight: .infinity, alignment: .center)
391391
.onTapGestureForced { UIApplication.shared.endEditing() }
392392
}
393393
}

0 commit comments

Comments
 (0)