Refactor tenders pagination and enhance API integration
- Updated TendersRepository and TendersService to support cursor-based pagination. - Modified TendersViewModel to manage pagination state and handle API responses more effectively. - Replaced existing pagination UI components with a new WindowedPagination widget across desktop, mobile, and tablet views. - Improved notification handling in the UI to reflect the presence of notifications dynamically. - Adjusted main tenders slider to ensure proper rendering based on the current page index.
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:tm_app/core/utils/logger.dart';
|
||||
import 'package:tm_app/data/repositories/tenders_repository.dart';
|
||||
import 'package:tm_app/data/services/model/feedback_data/feedback_data.dart';
|
||||
import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart';
|
||||
import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
|
||||
import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart';
|
||||
import 'package:tm_app/views/tenders/strings/tenders_strings.dart';
|
||||
|
||||
@@ -36,15 +37,44 @@ class TendersViewModel with ChangeNotifier {
|
||||
|
||||
List<FeedbackData> feedbacks = [];
|
||||
|
||||
int currentPage = 1;
|
||||
int _currentOffset = 0;
|
||||
int _totalPages = 0;
|
||||
int get totalPages => _totalPages;
|
||||
static const int _pageSize = 10;
|
||||
bool _hasMoreData = true;
|
||||
|
||||
// --- Offset pagination state ---
|
||||
int _currentOffset = 0;
|
||||
int _lastValidPages = 1;
|
||||
int _lastValidLimit = _pageSize;
|
||||
|
||||
bool _hasMoreData = true;
|
||||
bool get hasMoreData => _hasMoreData;
|
||||
|
||||
/// 0-based current page index.
|
||||
int get currentPageIndex {
|
||||
final meta = _tendersResponse?.meta;
|
||||
final limit = meta?.limit ?? _lastValidLimit;
|
||||
|
||||
if (limit > 0) {
|
||||
return _currentOffset ~/ limit;
|
||||
}
|
||||
if (meta?.offset != null && limit > 0) {
|
||||
return meta!.offset! ~/ limit;
|
||||
}
|
||||
if (meta?.page != null) {
|
||||
return meta!.page! - 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 1-based current page (for display).
|
||||
int get currentPage => currentPageIndex + 1;
|
||||
|
||||
int get totalPages {
|
||||
final pages = _tendersResponse?.meta?.pages;
|
||||
if (pages != null && pages > 0) {
|
||||
return pages;
|
||||
}
|
||||
return _lastValidPages;
|
||||
}
|
||||
|
||||
// Filter States
|
||||
DateTimeRange? selectedDateRange;
|
||||
String selectedStatus = 'All';
|
||||
@@ -80,15 +110,12 @@ class TendersViewModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
void setStatus(String status) {
|
||||
// Clear data when status changes
|
||||
_clearAllData();
|
||||
selectedStatus = status;
|
||||
currentPage = 1;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setSort(String sort) {
|
||||
// Clear data when status changes
|
||||
_clearAllData();
|
||||
selectedSort = sort;
|
||||
if (sort == TendersStrings.minimumTime) {
|
||||
@@ -98,16 +125,13 @@ class TendersViewModel with ChangeNotifier {
|
||||
sortBy = 'submission_deadline';
|
||||
sortOrder = 'desc';
|
||||
}
|
||||
currentPage = 1;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _clearAllData() {
|
||||
_tendersResponse = null;
|
||||
_errorMessage = null;
|
||||
_currentOffset = 0;
|
||||
_hasMoreData = true;
|
||||
currentPage = 1;
|
||||
_resetPagination();
|
||||
sortBy = '';
|
||||
sortOrder = '';
|
||||
selectedSort = '';
|
||||
@@ -116,6 +140,11 @@ class TendersViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _resetPagination() {
|
||||
_currentOffset = 0;
|
||||
_hasMoreData = true;
|
||||
}
|
||||
|
||||
void init(BuildContext context) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
@@ -151,10 +180,7 @@ class TendersViewModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> getTendersMobile({String? query}) async {
|
||||
// Clear all data before fetching new tenders
|
||||
|
||||
_currentOffset = 0;
|
||||
_hasMoreData = true;
|
||||
_resetPagination();
|
||||
_searchQuery = query;
|
||||
|
||||
final result = await _tendersRepository.getTenders(
|
||||
@@ -164,28 +190,7 @@ class TendersViewModel with ChangeNotifier {
|
||||
sortBy: '',
|
||||
sortOrder: '',
|
||||
);
|
||||
switch (result) {
|
||||
case Ok<TendersResponse>():
|
||||
_tendersResponse = result.value;
|
||||
// Check if we have more data
|
||||
if (_tendersResponse?.data?.tenders != null) {
|
||||
_hasMoreData = _tendersResponse!.data!.tenders!.length >= _pageSize;
|
||||
}
|
||||
if (_tendersResponse?.data?.tenders?.isNotEmpty == true) {
|
||||
// Fetch feedback and approvals for the first tender only
|
||||
getTenderFeedback(_tendersResponse!.data!.tenders!.first.id!);
|
||||
}
|
||||
if (_tendersResponse?.meta?.pages != null) {
|
||||
_totalPages = _tendersResponse!.meta!.pages!;
|
||||
} else {
|
||||
_totalPages = 1;
|
||||
}
|
||||
notifyListeners();
|
||||
break;
|
||||
case Error<TendersResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
break;
|
||||
}
|
||||
_handleListResult(result, fetchAllFeedbacks: false);
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
_errorMessage = null;
|
||||
@@ -199,11 +204,9 @@ class TendersViewModel with ChangeNotifier {
|
||||
String? publicationDateFrom,
|
||||
String? publicationDateTo,
|
||||
}) async {
|
||||
// Clear all data before fetching new tenders
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
_currentOffset = 0;
|
||||
_hasMoreData = true;
|
||||
_resetPagination();
|
||||
_searchQuery = query;
|
||||
|
||||
final result = await _tendersRepository.getTenders(
|
||||
@@ -215,35 +218,55 @@ class TendersViewModel with ChangeNotifier {
|
||||
publicationDateFrom: publicationDateFrom ?? '',
|
||||
publicationDateTo: publicationDateTo ?? '',
|
||||
);
|
||||
switch (result) {
|
||||
case Ok<TendersResponse>():
|
||||
_tendersResponse = result.value;
|
||||
// Check if we have more data
|
||||
if (_tendersResponse?.data?.tenders != null) {
|
||||
_hasMoreData = _tendersResponse!.data!.tenders!.length >= _pageSize;
|
||||
}
|
||||
if (_tendersResponse?.data?.tenders?.isNotEmpty == true) {
|
||||
for (var tender in _tendersResponse!.data!.tenders!) {
|
||||
getTenderFeedback(tender.id!);
|
||||
}
|
||||
}
|
||||
if (_tendersResponse?.meta?.pages != null) {
|
||||
_totalPages = _tendersResponse!.meta!.pages!;
|
||||
} else {
|
||||
_totalPages = 1;
|
||||
}
|
||||
notifyListeners();
|
||||
break;
|
||||
case Error<TendersResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
break;
|
||||
}
|
||||
_handleListResult(result, fetchAllFeedbacks: true);
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleListResult(
|
||||
Result<TendersResponse> result, {
|
||||
required bool fetchAllFeedbacks,
|
||||
}) {
|
||||
switch (result) {
|
||||
case Ok<TendersResponse>():
|
||||
_tendersResponse = result.value;
|
||||
final meta = _tendersResponse?.meta;
|
||||
if (meta?.pages != null && meta!.pages! > 0) {
|
||||
_lastValidPages = meta.pages!;
|
||||
}
|
||||
if (meta?.limit != null && meta!.limit! > 0) {
|
||||
_lastValidLimit = meta.limit!;
|
||||
}
|
||||
if (meta?.offset != null) {
|
||||
_currentOffset = meta!.offset!;
|
||||
}
|
||||
final tenders = _tendersResponse?.data?.tenders ?? [];
|
||||
_hasMoreData =
|
||||
meta?.hasMore ?? (tenders.length >= _pageSize);
|
||||
if (tenders.isNotEmpty) {
|
||||
if (fetchAllFeedbacks) {
|
||||
for (final tender in tenders) {
|
||||
if (tender.id != null) {
|
||||
getTenderFeedback(tender.id!);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getTenderFeedback(tenders.first.id!);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Error<TendersResponse>():
|
||||
_handleListError(result.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleListError(Exception error) {
|
||||
_errorMessage = error.toString();
|
||||
}
|
||||
|
||||
Future<void> loadMoreTenders() async {
|
||||
if (_isLoadingMore || !_hasMoreData) {
|
||||
return;
|
||||
@@ -253,7 +276,10 @@ class TendersViewModel with ChangeNotifier {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
_currentOffset += _pageSize;
|
||||
final meta = _tendersResponse?.meta;
|
||||
final limit = meta?.limit ?? _pageSize;
|
||||
_currentOffset += limit;
|
||||
|
||||
final result = await _tendersRepository.getTenders(
|
||||
limit: _pageSize,
|
||||
offset: _currentOffset,
|
||||
@@ -264,30 +290,31 @@ class TendersViewModel with ChangeNotifier {
|
||||
|
||||
switch (result) {
|
||||
case Ok<TendersResponse>():
|
||||
if (result.value.data?.tenders != null &&
|
||||
result.value.data!.tenders!.isNotEmpty) {
|
||||
// Create a new modifiable list by combining existing and new tenders
|
||||
if (_tendersResponse?.data?.tenders != null) {
|
||||
final existingTenders = _tendersResponse!.data!.tenders!;
|
||||
final newTenders = result.value.data!.tenders!;
|
||||
final combinedTenders = [...existingTenders, ...newTenders];
|
||||
|
||||
// Create a new TendersData with the combined list
|
||||
_tendersResponse = _tendersResponse!.copyWith(
|
||||
data: _tendersResponse!.data!.copyWith(tenders: combinedTenders),
|
||||
);
|
||||
} else {
|
||||
_tendersResponse = result.value;
|
||||
final newTenders = result.value.data?.tenders ?? <TenderData>[];
|
||||
if (newTenders.isNotEmpty) {
|
||||
final existingTenders =
|
||||
_tendersResponse?.data?.tenders ?? <TenderData>[];
|
||||
final combined = <TenderData>[...existingTenders, ...newTenders];
|
||||
_tendersResponse = result.value.copyWith(
|
||||
data: result.value.data?.copyWith(tenders: combined),
|
||||
);
|
||||
final newMeta = result.value.meta;
|
||||
if (newMeta?.pages != null && newMeta!.pages! > 0) {
|
||||
_lastValidPages = newMeta.pages!;
|
||||
}
|
||||
// Check if we have more data
|
||||
_hasMoreData = result.value.data!.tenders!.length >= _pageSize;
|
||||
if (newMeta?.limit != null && newMeta!.limit! > 0) {
|
||||
_lastValidLimit = newMeta.limit!;
|
||||
}
|
||||
_hasMoreData =
|
||||
newMeta?.hasMore ?? (newTenders.length >= _pageSize);
|
||||
} else {
|
||||
_hasMoreData = false;
|
||||
}
|
||||
break;
|
||||
case Error<TendersResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
_currentOffset -= _pageSize; // Revert offset on error
|
||||
final limit = meta?.limit ?? _pageSize;
|
||||
_currentOffset = (_currentOffset - limit).clamp(0, 1 << 30);
|
||||
_handleListError(result.error);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -298,7 +325,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> getTenderFeedback(String tenderId) async {
|
||||
// _isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
@@ -308,7 +334,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
switch (result) {
|
||||
case Ok<FeedbackResponse>():
|
||||
_feedbackData = result.value.data;
|
||||
// Add or update _feedbackData in feedbacks list by tender id
|
||||
if (_feedbackData != null) {
|
||||
final index = feedbacks.indexWhere(
|
||||
(f) => f.tenderId == _feedbackData!.tenderId,
|
||||
@@ -331,7 +356,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
_errorMessage = result.error.toString();
|
||||
break;
|
||||
}
|
||||
// _isLoading = false;
|
||||
notifyListeners();
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
@@ -343,38 +367,32 @@ class TendersViewModel with ChangeNotifier {
|
||||
}) async {
|
||||
_feedbackErrorMessage = null;
|
||||
|
||||
// Get current feedback for this tender
|
||||
final existingFeedback = getFeedbackForTender(tenderId);
|
||||
final existingFeedbackIndex = feedbacks.indexWhere(
|
||||
(f) => f.tenderId == tenderId,
|
||||
);
|
||||
|
||||
// Create optimistic feedback data
|
||||
FeedbackData? optimisticFeedback;
|
||||
|
||||
if (existingFeedback == null) {
|
||||
// No existing feedback - create new optimistic feedback
|
||||
optimisticFeedback = FeedbackData(
|
||||
companyId: null, // Will be filled by server
|
||||
companyId: null,
|
||||
createdAt: DateTime.now().millisecondsSinceEpoch,
|
||||
customerId: null, // Will be filled by server
|
||||
customerId: null,
|
||||
feedbackType: feedbackType,
|
||||
id: null, // Will be filled by server
|
||||
id: null,
|
||||
tenderId: tenderId,
|
||||
tender: null,
|
||||
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
feedbacks.add(optimisticFeedback);
|
||||
} else {
|
||||
// Existing feedback - update optimistically
|
||||
if (existingFeedback.feedbackType == feedbackType) {
|
||||
// Same feedback type - remove feedback (toggle off)
|
||||
if (existingFeedbackIndex != -1) {
|
||||
feedbacks.removeAt(existingFeedbackIndex);
|
||||
}
|
||||
optimisticFeedback = null;
|
||||
} else {
|
||||
// Different feedback type - update to new type
|
||||
optimisticFeedback = existingFeedback.copyWith(
|
||||
feedbackType: feedbackType,
|
||||
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
@@ -385,10 +403,8 @@ class TendersViewModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Notify listeners immediately for optimistic update
|
||||
notifyListeners();
|
||||
|
||||
// Call API
|
||||
final result = await _tendersRepository.toggleFeedback(
|
||||
tenderId: tenderId,
|
||||
feedbackType: feedbackType,
|
||||
@@ -398,7 +414,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
case Ok<FeedbackResponse>():
|
||||
_feedbackData = result.value.data;
|
||||
if (_feedbackData != null) {
|
||||
// Update with complete data from server
|
||||
final index = feedbacks.indexWhere(
|
||||
(f) => f.tender == _feedbackData!.tender,
|
||||
);
|
||||
@@ -408,17 +423,13 @@ class TendersViewModel with ChangeNotifier {
|
||||
feedbacks.add(_feedbackData!);
|
||||
}
|
||||
} else {
|
||||
// Server returned null data - remove feedback
|
||||
feedbacks.removeWhere((f) => f.tenderId == tenderId);
|
||||
}
|
||||
break;
|
||||
case Error<FeedbackResponse>():
|
||||
// Revert optimistic update on error
|
||||
if (existingFeedback == null) {
|
||||
// Remove the optimistic feedback we added
|
||||
feedbacks.removeWhere((f) => f.tenderId == tenderId);
|
||||
} else {
|
||||
// Restore the original feedback - find current index and update, or add if not found
|
||||
final currentIndex = feedbacks.indexWhere(
|
||||
(f) => f.tenderId == tenderId,
|
||||
);
|
||||
@@ -437,7 +448,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear all feedbacks
|
||||
void clearFeedbacks() {
|
||||
feedbacks.clear();
|
||||
_feedbackData = null;
|
||||
@@ -445,13 +455,10 @@ class TendersViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear all tenders
|
||||
void clearTenders() {
|
||||
_tendersResponse = null;
|
||||
_errorMessage = null;
|
||||
_currentOffset = 0;
|
||||
_hasMoreData = true;
|
||||
currentPage = 1;
|
||||
_resetPagination();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -464,7 +471,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
/// Search tenders with query
|
||||
Future<void> searchTenders(String query) async {
|
||||
await getTendersDesktop(
|
||||
query: query.trim(),
|
||||
@@ -475,7 +481,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
/// Clear all data (tenders, feedbacks, and approvals)
|
||||
void clearAll() {
|
||||
clearTenders();
|
||||
clearFeedbacks();
|
||||
@@ -488,17 +493,32 @@ class TendersViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void jumpToPage(int page) async {
|
||||
final meta = tendersResponse?.meta;
|
||||
if (meta?.pages == null) {
|
||||
/// [selected] is the absolute 0-based target page index.
|
||||
Future<void> handlePaginationChange(int selected) async {
|
||||
if (selected < 0) {
|
||||
return;
|
||||
}
|
||||
if (page < 1 || page > meta!.pages!) {
|
||||
final maxPage = totalPages - 1;
|
||||
if (maxPage >= 0 && selected > maxPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentPage = page;
|
||||
_currentOffset = (page - 1) * _pageSize;
|
||||
if (selected == currentPageIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _goToOffsetPage(selected);
|
||||
}
|
||||
|
||||
Future<void> _goToOffsetPage(int pageIndex) async {
|
||||
final limit = _tendersResponse?.meta?.limit ?? _pageSize;
|
||||
_currentOffset = pageIndex * limit;
|
||||
await _fetchPage();
|
||||
}
|
||||
|
||||
Future<void> _fetchPage() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
final result = await _tendersRepository.getTenders(
|
||||
limit: _pageSize,
|
||||
@@ -506,17 +526,20 @@ class TendersViewModel with ChangeNotifier {
|
||||
query: _searchQuery,
|
||||
sortBy: sortBy,
|
||||
sortOrder: sortOrder,
|
||||
publicationDateFrom: startDateUnix,
|
||||
publicationDateTo: endDateUnix,
|
||||
);
|
||||
|
||||
switch (result) {
|
||||
case Ok<TendersResponse>():
|
||||
_tendersResponse = result.value;
|
||||
break;
|
||||
case Error<TendersResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
break;
|
||||
}
|
||||
|
||||
_handleListResult(result, fetchAllFeedbacks: true);
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Kept for backwards compatibility with existing callers.
|
||||
/// Accepts a 1-based page number.
|
||||
void jumpToPage(int page) {
|
||||
handlePaginationChange(page - 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user