ae08b946f6
- 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.
546 lines
14 KiB
Dart
546 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:tm_app/core/utils/device_utils.dart';
|
|
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';
|
|
|
|
import '../core/utils/result.dart';
|
|
|
|
class TendersViewModel with ChangeNotifier {
|
|
final TendersRepository _tendersRepository;
|
|
|
|
TendersViewModel({required TendersRepository tendersRepository})
|
|
: _tendersRepository = tendersRepository;
|
|
|
|
bool _isLoading = false;
|
|
bool _isLoadingMore = false;
|
|
String? _errorMessage;
|
|
String? _feedbackErrorMessage;
|
|
TendersResponse? _tendersResponse;
|
|
FeedbackData? _feedbackData;
|
|
String? _searchQuery;
|
|
|
|
bool get isLoading => _isLoading;
|
|
bool get isLoadingMore => _isLoadingMore;
|
|
String? get errorMessage => _errorMessage;
|
|
String? get feedbackErrorMessage => _feedbackErrorMessage;
|
|
|
|
TendersResponse? get tendersResponse => _tendersResponse;
|
|
FeedbackData? get feedbackData => _feedbackData;
|
|
|
|
String? get searchQuery => _searchQuery;
|
|
|
|
List<FeedbackData> feedbacks = [];
|
|
|
|
static const int _pageSize = 10;
|
|
|
|
// --- 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';
|
|
String selectedSort = '';
|
|
String sortBy = '';
|
|
String sortOrder = '';
|
|
final _fmt = DateFormat('yyyy/MM/dd');
|
|
|
|
String get startDateFormatted =>
|
|
selectedDateRange?.start != null
|
|
? _fmt.format(selectedDateRange!.start)
|
|
: 'YYYY/MM/DD';
|
|
|
|
String get endDateFormatted =>
|
|
selectedDateRange?.end != null
|
|
? _fmt.format(selectedDateRange!.end)
|
|
: 'YYYY/MM/DD';
|
|
|
|
String get startDateUnix =>
|
|
selectedDateRange?.start != null
|
|
? (selectedDateRange!.start.millisecondsSinceEpoch ~/ 1000).toString()
|
|
: '';
|
|
|
|
String get endDateUnix =>
|
|
selectedDateRange?.end != null
|
|
? (selectedDateRange!.end.millisecondsSinceEpoch ~/ 1000).toString()
|
|
: '';
|
|
|
|
// --- Filter Actions ---
|
|
void setDateRange(DateTimeRange? range) {
|
|
selectedDateRange = range;
|
|
notifyListeners();
|
|
}
|
|
|
|
void setStatus(String status) {
|
|
_clearAllData();
|
|
selectedStatus = status;
|
|
notifyListeners();
|
|
}
|
|
|
|
void setSort(String sort) {
|
|
_clearAllData();
|
|
selectedSort = sort;
|
|
if (sort == TendersStrings.minimumTime) {
|
|
sortBy = 'submission_deadline';
|
|
sortOrder = 'asc';
|
|
} else if (sort == TendersStrings.mostOfTheTime) {
|
|
sortBy = 'submission_deadline';
|
|
sortOrder = 'desc';
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
void _clearAllData() {
|
|
_tendersResponse = null;
|
|
_errorMessage = null;
|
|
_resetPagination();
|
|
sortBy = '';
|
|
sortOrder = '';
|
|
selectedSort = '';
|
|
selectedStatus = '';
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
void _resetPagination() {
|
|
_currentOffset = 0;
|
|
_hasMoreData = true;
|
|
}
|
|
|
|
void init(BuildContext context) async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
clearAll();
|
|
await getTenders(context: context);
|
|
}
|
|
|
|
/// Get feedback for a specific tender
|
|
FeedbackData? getFeedbackForTender(String tenderId) {
|
|
try {
|
|
return feedbacks.firstWhere((f) => f.tenderId == tenderId);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Check if feedback exists for a specific tender
|
|
bool hasFeedbackForTender(String tenderId) {
|
|
return feedbacks.any((f) => f.tenderId == tenderId);
|
|
}
|
|
|
|
Future<void> getTenders({
|
|
required BuildContext context,
|
|
String? query,
|
|
}) async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
if (DeviceUtils.isDesktop(context)) {
|
|
await getTendersDesktop(query: query);
|
|
} else {
|
|
await getTendersMobile(query: query);
|
|
}
|
|
}
|
|
|
|
Future<void> getTendersMobile({String? query}) async {
|
|
_resetPagination();
|
|
_searchQuery = query;
|
|
|
|
final result = await _tendersRepository.getTenders(
|
|
limit: _pageSize,
|
|
offset: _currentOffset,
|
|
query: query,
|
|
sortBy: '',
|
|
sortOrder: '',
|
|
);
|
|
_handleListResult(result, fetchAllFeedbacks: false);
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> getTendersDesktop({
|
|
String? query,
|
|
String? sortBy,
|
|
String? sortOrder,
|
|
String? publicationDateFrom,
|
|
String? publicationDateTo,
|
|
}) async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
_resetPagination();
|
|
_searchQuery = query;
|
|
|
|
final result = await _tendersRepository.getTenders(
|
|
limit: _pageSize,
|
|
offset: _currentOffset,
|
|
query: query,
|
|
sortBy: sortBy ?? '',
|
|
sortOrder: sortOrder ?? '',
|
|
publicationDateFrom: publicationDateFrom ?? '',
|
|
publicationDateTo: publicationDateTo ?? '',
|
|
);
|
|
_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;
|
|
}
|
|
|
|
_isLoadingMore = true;
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
|
|
final meta = _tendersResponse?.meta;
|
|
final limit = meta?.limit ?? _pageSize;
|
|
_currentOffset += limit;
|
|
|
|
final result = await _tendersRepository.getTenders(
|
|
limit: _pageSize,
|
|
offset: _currentOffset,
|
|
query: _searchQuery,
|
|
sortBy: sortBy,
|
|
sortOrder: sortOrder,
|
|
);
|
|
|
|
switch (result) {
|
|
case Ok<TendersResponse>():
|
|
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!;
|
|
}
|
|
if (newMeta?.limit != null && newMeta!.limit! > 0) {
|
|
_lastValidLimit = newMeta.limit!;
|
|
}
|
|
_hasMoreData =
|
|
newMeta?.hasMore ?? (newTenders.length >= _pageSize);
|
|
} else {
|
|
_hasMoreData = false;
|
|
}
|
|
break;
|
|
case Error<TendersResponse>():
|
|
final limit = meta?.limit ?? _pageSize;
|
|
_currentOffset = (_currentOffset - limit).clamp(0, 1 << 30);
|
|
_handleListError(result.error);
|
|
break;
|
|
}
|
|
|
|
_isLoadingMore = false;
|
|
notifyListeners();
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> getTenderFeedback(String tenderId) async {
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
|
|
final result = await _tendersRepository.getTenderFeedback(
|
|
tenderId: tenderId,
|
|
);
|
|
switch (result) {
|
|
case Ok<FeedbackResponse>():
|
|
_feedbackData = result.value.data;
|
|
if (_feedbackData != null) {
|
|
final index = feedbacks.indexWhere(
|
|
(f) => f.tenderId == _feedbackData!.tenderId,
|
|
);
|
|
if (index != -1) {
|
|
feedbacks[index] = _feedbackData!;
|
|
AppLogger().info(
|
|
'Updated feedback for tender: ${_feedbackData!.tenderId}',
|
|
);
|
|
} else {
|
|
feedbacks.add(_feedbackData!);
|
|
AppLogger().info(
|
|
'Added new feedback for tender: ${_feedbackData!.tenderId}',
|
|
);
|
|
}
|
|
AppLogger().info('Total feedbacks: ${feedbacks.length}');
|
|
}
|
|
break;
|
|
case Error<FeedbackResponse>():
|
|
_errorMessage = result.error.toString();
|
|
break;
|
|
}
|
|
notifyListeners();
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> toogleFeedback({
|
|
required String tenderId,
|
|
required String feedbackType,
|
|
}) async {
|
|
_feedbackErrorMessage = null;
|
|
|
|
final existingFeedback = getFeedbackForTender(tenderId);
|
|
final existingFeedbackIndex = feedbacks.indexWhere(
|
|
(f) => f.tenderId == tenderId,
|
|
);
|
|
|
|
FeedbackData? optimisticFeedback;
|
|
|
|
if (existingFeedback == null) {
|
|
optimisticFeedback = FeedbackData(
|
|
companyId: null,
|
|
createdAt: DateTime.now().millisecondsSinceEpoch,
|
|
customerId: null,
|
|
feedbackType: feedbackType,
|
|
id: null,
|
|
tenderId: tenderId,
|
|
tender: null,
|
|
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
|
);
|
|
feedbacks.add(optimisticFeedback);
|
|
} else {
|
|
if (existingFeedback.feedbackType == feedbackType) {
|
|
if (existingFeedbackIndex != -1) {
|
|
feedbacks.removeAt(existingFeedbackIndex);
|
|
}
|
|
optimisticFeedback = null;
|
|
} else {
|
|
optimisticFeedback = existingFeedback.copyWith(
|
|
feedbackType: feedbackType,
|
|
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
|
);
|
|
if (existingFeedbackIndex != -1) {
|
|
feedbacks[existingFeedbackIndex] = optimisticFeedback;
|
|
}
|
|
}
|
|
}
|
|
|
|
notifyListeners();
|
|
|
|
final result = await _tendersRepository.toggleFeedback(
|
|
tenderId: tenderId,
|
|
feedbackType: feedbackType,
|
|
);
|
|
|
|
switch (result) {
|
|
case Ok<FeedbackResponse>():
|
|
_feedbackData = result.value.data;
|
|
if (_feedbackData != null) {
|
|
final index = feedbacks.indexWhere(
|
|
(f) => f.tender == _feedbackData!.tender,
|
|
);
|
|
if (index != -1) {
|
|
feedbacks[index] = _feedbackData!;
|
|
} else {
|
|
feedbacks.add(_feedbackData!);
|
|
}
|
|
} else {
|
|
feedbacks.removeWhere((f) => f.tenderId == tenderId);
|
|
}
|
|
break;
|
|
case Error<FeedbackResponse>():
|
|
if (existingFeedback == null) {
|
|
feedbacks.removeWhere((f) => f.tenderId == tenderId);
|
|
} else {
|
|
final currentIndex = feedbacks.indexWhere(
|
|
(f) => f.tenderId == tenderId,
|
|
);
|
|
if (currentIndex != -1) {
|
|
feedbacks[currentIndex] = existingFeedback;
|
|
} else {
|
|
feedbacks.add(existingFeedback);
|
|
}
|
|
}
|
|
_feedbackErrorMessage = result.error.toString();
|
|
break;
|
|
}
|
|
|
|
notifyListeners();
|
|
_feedbackErrorMessage = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
void clearFeedbacks() {
|
|
feedbacks.clear();
|
|
_feedbackData = null;
|
|
_feedbackErrorMessage = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
void clearTenders() {
|
|
_tendersResponse = null;
|
|
_errorMessage = null;
|
|
_resetPagination();
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> callWithSortAndStatus() async {
|
|
await getTendersDesktop(
|
|
sortBy: sortBy,
|
|
sortOrder: sortOrder,
|
|
publicationDateFrom: startDateUnix,
|
|
publicationDateTo: endDateUnix,
|
|
);
|
|
}
|
|
|
|
Future<void> searchTenders(String query) async {
|
|
await getTendersDesktop(
|
|
query: query.trim(),
|
|
sortBy: sortBy,
|
|
sortOrder: sortOrder,
|
|
publicationDateFrom: startDateUnix,
|
|
publicationDateTo: endDateUnix,
|
|
);
|
|
}
|
|
|
|
void clearAll() {
|
|
clearTenders();
|
|
clearFeedbacks();
|
|
_searchQuery = null;
|
|
selectedSort = '';
|
|
selectedStatus = '';
|
|
selectedDateRange = null;
|
|
sortBy = '';
|
|
sortOrder = '';
|
|
notifyListeners();
|
|
}
|
|
|
|
/// [selected] is the absolute 0-based target page index.
|
|
Future<void> handlePaginationChange(int selected) async {
|
|
if (selected < 0) {
|
|
return;
|
|
}
|
|
final maxPage = totalPages - 1;
|
|
if (maxPage >= 0 && selected > maxPage) {
|
|
return;
|
|
}
|
|
|
|
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,
|
|
offset: _currentOffset,
|
|
query: _searchQuery,
|
|
sortBy: sortBy,
|
|
sortOrder: sortOrder,
|
|
publicationDateFrom: startDateUnix,
|
|
publicationDateTo: endDateUnix,
|
|
);
|
|
|
|
_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);
|
|
}
|
|
}
|