From cd32bbd8127a5316c80961694efa3c335ee1430e Mon Sep 17 00:00:00 2001 From: amirrezaghabeli Date: Sun, 7 Sep 2025 12:15:43 +0330 Subject: [PATCH] added pagination in your tenders and some fixes --- devtools_options.yaml | 3 + .../tender_approvals_data_wrapper.dart | 17 +- ...tender_approvals_data_wrapper.freezed.dart | 305 +----------------- .../tender_approvals_data_wrapper.g.dart | 18 +- lib/view_models/home_view_model.dart | 99 +++--- lib/view_models/your_tenders_view_model.dart | 118 ++++++- .../widgets/liked_tenders_list_item.dart | 4 +- .../pages/your_tenders_mobile_page.dart | 5 +- .../your_tenders/widgets/filter_button.dart | 6 +- .../widgets/tenders_filter_dialog.dart | 4 +- .../widgets/tenders_submitted.dart | 166 +++++++--- pubspec.lock | 16 +- 12 files changed, 313 insertions(+), 448 deletions(-) create mode 100644 devtools_options.yaml diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.dart b/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.dart index 9469cdc..fbbb875 100644 --- a/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.dart +++ b/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.dart @@ -1,4 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; +import '../meta/meta.dart'; import '../tender_approvals_data/tender_approvals_data.dart'; part 'tender_approvals_data_wrapper.freezed.dart'; @@ -8,23 +9,9 @@ part 'tender_approvals_data_wrapper.g.dart'; abstract class TenderApprovalsDataWrapper with _$TenderApprovalsDataWrapper { const factory TenderApprovalsDataWrapper({ required List? tenders, - required Metadata? metadata, + required Meta? metadata, }) = _TenderApprovalsDataWrapper; factory TenderApprovalsDataWrapper.fromJson(Map json) => _$TenderApprovalsDataWrapperFromJson(json); } - -@freezed -abstract class Metadata with _$Metadata { - const factory Metadata({ - required int? total, - required int? limit, - required int? offset, - required int? page, - required int? pages, - }) = _Metadata; - - factory Metadata.fromJson(Map json) => - _$MetadataFromJson(json); -} diff --git a/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.freezed.dart b/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.freezed.dart index f341f47..27fe56b 100644 --- a/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.freezed.dart +++ b/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$TenderApprovalsDataWrapper { - List? get tenders; Metadata? get metadata; + List? get tenders; Meta? get metadata; /// Create a copy of TenderApprovalsDataWrapper /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -48,11 +48,11 @@ abstract mixin class $TenderApprovalsDataWrapperCopyWith<$Res> { factory $TenderApprovalsDataWrapperCopyWith(TenderApprovalsDataWrapper value, $Res Function(TenderApprovalsDataWrapper) _then) = _$TenderApprovalsDataWrapperCopyWithImpl; @useResult $Res call({ - List? tenders, Metadata? metadata + List? tenders, Meta? metadata }); -$MetadataCopyWith<$Res>? get metadata; +$MetaCopyWith<$Res>? get metadata; } /// @nodoc @@ -69,19 +69,19 @@ class _$TenderApprovalsDataWrapperCopyWithImpl<$Res> return _then(_self.copyWith( tenders: freezed == tenders ? _self.tenders : tenders // ignore: cast_nullable_to_non_nullable as List?,metadata: freezed == metadata ? _self.metadata : metadata // ignore: cast_nullable_to_non_nullable -as Metadata?, +as Meta?, )); } /// Create a copy of TenderApprovalsDataWrapper /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$MetadataCopyWith<$Res>? get metadata { +$MetaCopyWith<$Res>? get metadata { if (_self.metadata == null) { return null; } - return $MetadataCopyWith<$Res>(_self.metadata!, (value) { + return $MetaCopyWith<$Res>(_self.metadata!, (value) { return _then(_self.copyWith(metadata: value)); }); } @@ -166,7 +166,7 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( List? tenders, Metadata? metadata)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( List? tenders, Meta? metadata)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _TenderApprovalsDataWrapper() when $default != null: return $default(_that.tenders,_that.metadata);case _: @@ -187,7 +187,7 @@ return $default(_that.tenders,_that.metadata);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( List? tenders, Metadata? metadata) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( List? tenders, Meta? metadata) $default,) {final _that = this; switch (_that) { case _TenderApprovalsDataWrapper(): return $default(_that.tenders,_that.metadata);case _: @@ -207,7 +207,7 @@ return $default(_that.tenders,_that.metadata);case _: /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( List? tenders, Metadata? metadata)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List? tenders, Meta? metadata)? $default,) {final _that = this; switch (_that) { case _TenderApprovalsDataWrapper() when $default != null: return $default(_that.tenders,_that.metadata);case _: @@ -234,7 +234,7 @@ class _TenderApprovalsDataWrapper implements TenderApprovalsDataWrapper { return EqualUnmodifiableListView(value); } -@override final Metadata? metadata; +@override final Meta? metadata; /// Create a copy of TenderApprovalsDataWrapper /// with the given fields replaced by the non-null parameter values. @@ -269,11 +269,11 @@ abstract mixin class _$TenderApprovalsDataWrapperCopyWith<$Res> implements $Tend factory _$TenderApprovalsDataWrapperCopyWith(_TenderApprovalsDataWrapper value, $Res Function(_TenderApprovalsDataWrapper) _then) = __$TenderApprovalsDataWrapperCopyWithImpl; @override @useResult $Res call({ - List? tenders, Metadata? metadata + List? tenders, Meta? metadata }); -@override $MetadataCopyWith<$Res>? get metadata; +@override $MetaCopyWith<$Res>? get metadata; } /// @nodoc @@ -290,7 +290,7 @@ class __$TenderApprovalsDataWrapperCopyWithImpl<$Res> return _then(_TenderApprovalsDataWrapper( tenders: freezed == tenders ? _self._tenders : tenders // ignore: cast_nullable_to_non_nullable as List?,metadata: freezed == metadata ? _self.metadata : metadata // ignore: cast_nullable_to_non_nullable -as Metadata?, +as Meta?, )); } @@ -298,290 +298,15 @@ as Metadata?, /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$MetadataCopyWith<$Res>? get metadata { +$MetaCopyWith<$Res>? get metadata { if (_self.metadata == null) { return null; } - return $MetadataCopyWith<$Res>(_self.metadata!, (value) { + return $MetaCopyWith<$Res>(_self.metadata!, (value) { return _then(_self.copyWith(metadata: value)); }); } } - -/// @nodoc -mixin _$Metadata { - - int? get total; int? get limit; int? get offset; int? get page; int? get pages; -/// Create a copy of Metadata -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$MetadataCopyWith get copyWith => _$MetadataCopyWithImpl(this as Metadata, _$identity); - - /// Serializes this Metadata to a JSON map. - Map toJson(); - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is Metadata&&(identical(other.total, total) || other.total == total)&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.page, page) || other.page == page)&&(identical(other.pages, pages) || other.pages == pages)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,total,limit,offset,page,pages); - -@override -String toString() { - return 'Metadata(total: $total, limit: $limit, offset: $offset, page: $page, pages: $pages)'; -} - - -} - -/// @nodoc -abstract mixin class $MetadataCopyWith<$Res> { - factory $MetadataCopyWith(Metadata value, $Res Function(Metadata) _then) = _$MetadataCopyWithImpl; -@useResult -$Res call({ - int? total, int? limit, int? offset, int? page, int? pages -}); - - - - -} -/// @nodoc -class _$MetadataCopyWithImpl<$Res> - implements $MetadataCopyWith<$Res> { - _$MetadataCopyWithImpl(this._self, this._then); - - final Metadata _self; - final $Res Function(Metadata) _then; - -/// Create a copy of Metadata -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? total = freezed,Object? limit = freezed,Object? offset = freezed,Object? page = freezed,Object? pages = freezed,}) { - return _then(_self.copyWith( -total: freezed == total ? _self.total : total // ignore: cast_nullable_to_non_nullable -as int?,limit: freezed == limit ? _self.limit : limit // ignore: cast_nullable_to_non_nullable -as int?,offset: freezed == offset ? _self.offset : offset // ignore: cast_nullable_to_non_nullable -as int?,page: freezed == page ? _self.page : page // ignore: cast_nullable_to_non_nullable -as int?,pages: freezed == pages ? _self.pages : pages // ignore: cast_nullable_to_non_nullable -as int?, - )); -} - -} - - -/// Adds pattern-matching-related methods to [Metadata]. -extension MetadataPatterns on Metadata { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _Metadata value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _Metadata() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _Metadata value) $default,){ -final _that = this; -switch (_that) { -case _Metadata(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Metadata value)? $default,){ -final _that = this; -switch (_that) { -case _Metadata() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( int? total, int? limit, int? offset, int? page, int? pages)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _Metadata() when $default != null: -return $default(_that.total,_that.limit,_that.offset,_that.page,_that.pages);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( int? total, int? limit, int? offset, int? page, int? pages) $default,) {final _that = this; -switch (_that) { -case _Metadata(): -return $default(_that.total,_that.limit,_that.offset,_that.page,_that.pages);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( int? total, int? limit, int? offset, int? page, int? pages)? $default,) {final _that = this; -switch (_that) { -case _Metadata() when $default != null: -return $default(_that.total,_that.limit,_that.offset,_that.page,_that.pages);case _: - return null; - -} -} - -} - -/// @nodoc -@JsonSerializable() - -class _Metadata implements Metadata { - const _Metadata({required this.total, required this.limit, required this.offset, required this.page, required this.pages}); - factory _Metadata.fromJson(Map json) => _$MetadataFromJson(json); - -@override final int? total; -@override final int? limit; -@override final int? offset; -@override final int? page; -@override final int? pages; - -/// Create a copy of Metadata -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$MetadataCopyWith<_Metadata> get copyWith => __$MetadataCopyWithImpl<_Metadata>(this, _$identity); - -@override -Map toJson() { - return _$MetadataToJson(this, ); -} - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _Metadata&&(identical(other.total, total) || other.total == total)&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.page, page) || other.page == page)&&(identical(other.pages, pages) || other.pages == pages)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,total,limit,offset,page,pages); - -@override -String toString() { - return 'Metadata(total: $total, limit: $limit, offset: $offset, page: $page, pages: $pages)'; -} - - -} - -/// @nodoc -abstract mixin class _$MetadataCopyWith<$Res> implements $MetadataCopyWith<$Res> { - factory _$MetadataCopyWith(_Metadata value, $Res Function(_Metadata) _then) = __$MetadataCopyWithImpl; -@override @useResult -$Res call({ - int? total, int? limit, int? offset, int? page, int? pages -}); - - - - -} -/// @nodoc -class __$MetadataCopyWithImpl<$Res> - implements _$MetadataCopyWith<$Res> { - __$MetadataCopyWithImpl(this._self, this._then); - - final _Metadata _self; - final $Res Function(_Metadata) _then; - -/// Create a copy of Metadata -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? total = freezed,Object? limit = freezed,Object? offset = freezed,Object? page = freezed,Object? pages = freezed,}) { - return _then(_Metadata( -total: freezed == total ? _self.total : total // ignore: cast_nullable_to_non_nullable -as int?,limit: freezed == limit ? _self.limit : limit // ignore: cast_nullable_to_non_nullable -as int?,offset: freezed == offset ? _self.offset : offset // ignore: cast_nullable_to_non_nullable -as int?,page: freezed == page ? _self.page : page // ignore: cast_nullable_to_non_nullable -as int?,pages: freezed == pages ? _self.pages : pages // ignore: cast_nullable_to_non_nullable -as int?, - )); -} - - -} - // dart format on diff --git a/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.g.dart b/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.g.dart index 8ccc748..6a4830b 100644 --- a/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.g.dart +++ b/lib/data/services/model/tender_approvals_data_warpper/tender_approvals_data_wrapper.g.dart @@ -16,7 +16,7 @@ _TenderApprovalsDataWrapper _$TenderApprovalsDataWrapperFromJson( metadata: json['metadata'] == null ? null - : Metadata.fromJson(json['metadata'] as Map), + : Meta.fromJson(json['metadata'] as Map), ); Map _$TenderApprovalsDataWrapperToJson( @@ -25,19 +25,3 @@ Map _$TenderApprovalsDataWrapperToJson( 'tenders': instance.tenders, 'metadata': instance.metadata, }; - -_Metadata _$MetadataFromJson(Map json) => _Metadata( - total: (json['total'] as num?)?.toInt(), - limit: (json['limit'] as num?)?.toInt(), - offset: (json['offset'] as num?)?.toInt(), - page: (json['page'] as num?)?.toInt(), - pages: (json['pages'] as num?)?.toInt(), -); - -Map _$MetadataToJson(_Metadata instance) => { - 'total': instance.total, - 'limit': instance.limit, - 'offset': instance.offset, - 'page': instance.page, - 'pages': instance.pages, -}; diff --git a/lib/view_models/home_view_model.dart b/lib/view_models/home_view_model.dart index 211c52c..f779783 100644 --- a/lib/view_models/home_view_model.dart +++ b/lib/view_models/home_view_model.dart @@ -14,7 +14,7 @@ class HomeViewModel with ChangeNotifier { final HomeRepository _homeRepository; HomeViewModel({required HomeRepository homeRepository}) - : _homeRepository = homeRepository { + : _homeRepository = homeRepository { init(); } @@ -31,7 +31,7 @@ class HomeViewModel with ChangeNotifier { TenderApprovalsStateResponse? tenderApprovalsStateResponse; FeedbackStats? feedbackStats; final List _tenders = []; - final int _pageSize = 20; + final int _pageSize = 10; int _currentPage = 0; bool _isLoadingMore = false; bool _hasMore = true; @@ -44,9 +44,15 @@ class HomeViewModel with ChangeNotifier { bool get isRecommendedMode => _isRecommendedMode; void init() async { + _isLoading = true; + notifyListeners(); + await getApprovedStates(); await getYourTenders(reset: true); await getFeedbackStats(); + + _isLoading = false; + notifyListeners(); } Future getHomeRecommendTenders({bool reset = false}) async { @@ -93,62 +99,59 @@ class HomeViewModel with ChangeNotifier { notifyListeners(); } -Future getYourTenders({bool reset = false}) async { - if (_isLoadingMore || (!_hasMore && !reset)) { - return; - } + Future getYourTenders({bool reset = false}) async { + if (_isLoadingMore || (!_hasMore && !reset)) { + return; + } - if (reset) { - _currentPage = 0; - _hasMore = true; - _tenders.clear(); - } + if (reset) { + _currentPage = 0; + _hasMore = true; + _tenders.clear(); + } - _isLoadingMore = true; - _errorMessage = null; - _isRecommendedMode = false; - notifyListeners(); + _isLoadingMore = true; + _errorMessage = null; + _isRecommendedMode = false; + notifyListeners(); - final result = await _homeRepository.getYourTenders( - limit: _pageSize, - offset: _currentPage * _pageSize, - ); + final result = await _homeRepository.getYourTenders( + limit: _pageSize, + offset: _currentPage * _pageSize, + ); - switch (result) { - case Ok(): - data = result.value; + switch (result) { + case Ok(): + data = result.value; - final tendersList = data?.data?.tenders ?? []; - final metadata = data?.data?.metadata; + final tendersList = data?.data?.tenders ?? []; + final metadata = data?.data?.metadata; - if (tendersList.isNotEmpty) { - _tenders.addAll( - tendersList.map((e) => e.tender).whereType(), - ); - _currentPage++; + if (tendersList.isNotEmpty) { + _tenders.addAll( + tendersList.map((e) => e.tender).whereType(), + ); + _currentPage++; - if (metadata != null && (_currentPage >= metadata.pages!)) { + if (metadata != null && (_currentPage >= metadata.pages!)) { + _hasMore = false; + } + } else { _hasMore = false; } - } else { - _hasMore = false; - } - break; + break; - case Error(): - _errorMessage = result.error.toString(); - break; + case Error(): + _errorMessage = result.error.toString(); + break; + } + + _isLoadingMore = false; + notifyListeners(); } - _isLoadingMore = false; - notifyListeners(); -} - - Future getApprovedStates() async { - _isLoading = true; _errorMessage = null; - notifyListeners(); final result = await _homeRepository.getTendersApprovalsState(); @@ -160,15 +163,10 @@ Future getYourTenders({bool reset = false}) async { _errorMessage = result.error.toString(); break; } - - _isLoading = false; - notifyListeners(); } Future getFeedbackStats() async { - _isLoading = true; _errorMessage = null; - notifyListeners(); final result = await _homeRepository.getFeedbackStats(); @@ -180,9 +178,6 @@ Future getYourTenders({bool reset = false}) async { _errorMessage = result.error.toString(); break; } - - _isLoading = false; - notifyListeners(); } double get partnershipCount { diff --git a/lib/view_models/your_tenders_view_model.dart b/lib/view_models/your_tenders_view_model.dart index caf70da..6fd17a7 100644 --- a/lib/view_models/your_tenders_view_model.dart +++ b/lib/view_models/your_tenders_view_model.dart @@ -20,6 +20,12 @@ class YourTendersViewModel with ChangeNotifier { int _offset = 0; final int _limit = 10; String? _currentFeedbackType; + + // Pagination state for tenders + bool isLoadingMoreTenders = false; + bool hasMoreTenders = true; + int _tendersOffset = 0; + final int _tendersLimit = 10; // Filter States DateTimeRange? selectedDateRange; String selectedStatus = 'All'; @@ -48,20 +54,53 @@ class YourTendersViewModel with ChangeNotifier { Future getApprovedTenders({ required int limit, required int offset, + bool reset = false, }) async { - isLoading = true; + if (!reset && (isLoadingMoreTenders || !hasMoreTenders)) { + return; + } + + if (reset) { + _tendersOffset = 0; + hasMoreTenders = true; + data = null; + } + + if (_tendersOffset == 0) { + isLoading = true; + } else { + isLoadingMoreTenders = true; + } notifyListeners(); final result = await yourTendersRepository.getApprovedTenders( - limit: limit, - offset: offset, + limit: _tendersLimit, + offset: _tendersOffset, createdFrom: startDateUnix, createdTo: endDateUnix, ); switch (result) { case Ok(): - data = result.value; + final incoming = result.value; + final incomingList = incoming.data?.tenders ?? []; + + if (reset || data == null) { + data = incoming; + } else { + final existingList = data!.data?.tenders ?? []; + final combined = List.of(existingList)..addAll(incomingList); + final newMetadata = incoming.data?.metadata ?? data!.data?.metadata; + data = data!.copyWith( + data: data!.data!.copyWith( + tenders: combined, + metadata: newMetadata, + ), + ); + } + + hasMoreTenders = incomingList.length == _tendersLimit; + _tendersOffset += _tendersLimit; errorMessage = null; break; case Error(): @@ -70,6 +109,7 @@ class YourTendersViewModel with ChangeNotifier { } isLoading = false; + isLoadingMoreTenders = false; notifyListeners(); } @@ -78,19 +118,53 @@ class YourTendersViewModel with ChangeNotifier { required int offset, required String createdFrom, required String createdTo, + bool reset = false, }) async { - isLoading = true; + if (!reset && (isLoadingMoreTenders || !hasMoreTenders)) { + return; + } + + if (reset) { + _tendersOffset = 0; + hasMoreTenders = true; + data = null; + } + + if (_tendersOffset == 0) { + isLoading = true; + } else { + isLoadingMoreTenders = true; + } notifyListeners(); final result = await yourTendersRepository.getTenders( - limit: limit, - offset: offset, + limit: _tendersLimit, + offset: _tendersOffset, createdFrom: createdFrom, createdTo: createdTo, ); + switch (result) { case Ok(): - data = result.value; + final incoming = result.value; + final incomingList = incoming.data?.tenders ?? []; + + if (reset || data == null) { + data = incoming; + } else { + final existingList = data!.data?.tenders ?? []; + final combined = List.of(existingList)..addAll(incomingList); + final newMetadata = incoming.data?.metadata ?? data!.data?.metadata; + data = data!.copyWith( + data: data!.data!.copyWith( + tenders: combined, + metadata: newMetadata, + ), + ); + } + + hasMoreTenders = incomingList.length == _tendersLimit; + _tendersOffset += _tendersLimit; errorMessage = null; break; case Error(): @@ -99,6 +173,7 @@ class YourTendersViewModel with ChangeNotifier { } isLoading = false; + isLoadingMoreTenders = false; notifyListeners(); } @@ -171,6 +246,28 @@ class YourTendersViewModel with ChangeNotifier { await getTendersFeedback(feedbackType: _currentFeedbackType!, reset: false); } + Future loadMoreTenders() async { + if (isLoadingMoreTenders || !hasMoreTenders) { + return; + } + + if (selectedStatus == YourTendersStrings.all) { + await getTenders( + limit: _tendersLimit, + offset: _tendersOffset, + createdFrom: startDateUnix, + createdTo: endDateUnix, + reset: false, + ); + } else if (selectedStatus == YourTendersStrings.tendersSubmitted) { + await getApprovedTenders( + limit: _tendersLimit, + offset: _tendersOffset, + reset: false, + ); + } + } + // --- Filter Actions --- void setDateRange(DateTimeRange? range) { selectedDateRange = range; @@ -191,13 +288,14 @@ class YourTendersViewModel with ChangeNotifier { Future callWithFilter() async { if (selectedStatus == YourTendersStrings.all) { await getTenders( - limit: 10, + limit: _tendersLimit, offset: 0, createdFrom: startDateUnix, createdTo: endDateUnix, + reset: true, ); } else if (selectedStatus == YourTendersStrings.tendersSubmitted) { - await getApprovedTenders(limit: 10, offset: 0); + await getApprovedTenders(limit: _tendersLimit, offset: 0, reset: true); } else if (selectedStatus == YourTendersStrings.likeTenders) { await getTendersFeedback(feedbackType: 'like', reset: true); } else if (selectedStatus == YourTendersStrings.dislikeTenders) { diff --git a/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart b/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart index 09a1063..8b8be25 100644 --- a/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart +++ b/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart @@ -53,7 +53,7 @@ class LikedListItem extends StatelessWidget { vertical: 5.0.h(), ), decoration: BoxDecoration( - color: AppColors.primary30, + color: AppColors.primary20, borderRadius: BorderRadius.circular(4), ), child: Row( @@ -100,7 +100,7 @@ class LikedListItem extends StatelessWidget { vertical: 5.0.h(), ), decoration: BoxDecoration( - color: AppColors.primary30, + color: AppColors.primary20, borderRadius: BorderRadius.circular(4), ), child: Row( diff --git a/lib/views/your_tenders/pages/your_tenders_mobile_page.dart b/lib/views/your_tenders/pages/your_tenders_mobile_page.dart index 80f92cc..80060c1 100644 --- a/lib/views/your_tenders/pages/your_tenders_mobile_page.dart +++ b/lib/views/your_tenders/pages/your_tenders_mobile_page.dart @@ -42,7 +42,10 @@ class _YourTendersMobilePageState extends State { Expanded( child: Consumer( builder: (context, viewModel, child) { - if (viewModel.isLoading) { + if (viewModel.isLoading && + (viewModel.data?.data?.tenders ?? []).isEmpty && + (viewModel.likedTendersData?.data?.feedback ?? []) + .isEmpty) { return const Center( child: CircularProgressIndicator( color: AppColors.secondary50, diff --git a/lib/views/your_tenders/widgets/filter_button.dart b/lib/views/your_tenders/widgets/filter_button.dart index 7493ff5..df23428 100644 --- a/lib/views/your_tenders/widgets/filter_button.dart +++ b/lib/views/your_tenders/widgets/filter_button.dart @@ -48,10 +48,10 @@ class FilterButton extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( - color: Colors.blue.shade50, + color: AppColors.primary20, borderRadius: BorderRadius.circular(12), ), - child: const Center( + child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -60,7 +60,7 @@ class FilterButton extends StatelessWidget { Text( YourTendersStrings.filter, style: TextStyle( - color: Colors.blue, + color: AppColors.textBlue, fontWeight: FontWeight.bold, ), ), diff --git a/lib/views/your_tenders/widgets/tenders_filter_dialog.dart b/lib/views/your_tenders/widgets/tenders_filter_dialog.dart index 714fef2..56ac63a 100644 --- a/lib/views/your_tenders/widgets/tenders_filter_dialog.dart +++ b/lib/views/your_tenders/widgets/tenders_filter_dialog.dart @@ -165,8 +165,8 @@ class TendersFilterDialog extends StatelessWidget { vm.callWithFilter(); }, text: YourTendersStrings.confirm, - textColor: AppColors.mainBlue, - backgroundColor: AppColors.primary30, + textColor: AppColors.textBlue, + backgroundColor: AppColors.primary20, ), ), ], diff --git a/lib/views/your_tenders/widgets/tenders_submitted.dart b/lib/views/your_tenders/widgets/tenders_submitted.dart index fd1bea8..55863e1 100644 --- a/lib/views/your_tenders/widgets/tenders_submitted.dart +++ b/lib/views/your_tenders/widgets/tenders_submitted.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/theme/colors.dart'; +import 'package:tm_app/core/utils/size_config.dart'; +import 'package:tm_app/view_models/your_tenders_view_model.dart'; import 'package:tm_app/views/your_tenders/widgets/tender_card.dart'; import '../../../data/services/model/tender_approvals_response/tender_approvals_response.dart'; -class TendersSubmitted extends StatelessWidget { +class TendersSubmitted extends StatefulWidget { const TendersSubmitted({ required this.approvedTenders, required this.isDesktop, @@ -15,57 +18,124 @@ class TendersSubmitted extends StatelessWidget { final TenderApprovalsResponse approvedTenders; final bool isDesktop; + @override + State createState() => _TendersSubmittedState(); +} + +class _TendersSubmittedState extends State { + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController()..addListener(_onScroll); + } + + void _onScroll() { + if (!_scrollController.hasClients) { + return; + } + final max = _scrollController.position.maxScrollExtent; + final pos = _scrollController.position.pixels; + const threshold = 200.0; + if (max - pos <= threshold) { + final vm = Provider.of(context, listen: false); + vm.loadMoreTenders(); + } + } + + @override + void dispose() { + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return approvedTenders.data == null - ? const Center(child: Text('No submitted projects yet.')) - : ListView.builder( - padding: const EdgeInsets.only(top: 20), - itemCount: approvedTenders.data!.tenders!.length, + return Consumer( + builder: (context, viewModel, child) { + if (widget.approvedTenders.data == null) { + return const Center(child: Text('No submitted projects yet.')); + } + + final tenders = widget.approvedTenders.data!.tenders ?? []; + if (tenders.isEmpty) { + return const Center(child: Text('No submitted projects yet.')); + } + + final itemCount = + tenders.length + (viewModel.isLoadingMoreTenders ? 1 : 0); + + return ListView.builder( + controller: _scrollController, + padding: EdgeInsets.symmetric( + // horizontal: 24.0.w(), + vertical: 15.0.h(), + ), + itemCount: itemCount, itemBuilder: (context, index) { - final tender = approvedTenders.data!.tenders![index]; - return TenderCard( - date: tender.createdAt!.toString(), - title: tender.tender!.title!, - description: tender.tender!.description!, - location: tender.tender!.countryCode!, - countryFlag: AssetsManager.seFlag, - status: tender.status, - countryCode: tender.tender!.countryCode!, - projectStatus: tender.tender!.status!, - isDesktop: isDesktop, - backgroundColor: - tender.status == 'Won' - ? AppColors.green0 - : tender.status == 'submitted' - ? AppColors.grey0 - : AppColors.red0.withValues(alpha: 0.2), - borderColor: - tender.status == 'Won' - ? AppColors.green10 - : tender.status == 'submitted' - ? AppColors.grey30 - : AppColors.red0, - statusIcon: - tender.status == 'Won' - ? AssetsManager.tickCircle - : tender.status == 'submitted' - ? null - : AssetsManager.closeCircle, - statusCardColor: - tender.status == 'Won' - ? AppColors.green20 - : tender.status == 'submitted' - ? AppColors.green20 - : AppColors.red0, - statusTextColor: - tender.status == 'Won' - ? AppColors.green30 - : tender.status == 'submitted' - ? AppColors.green30 - : AppColors.red10, - ); + if (index < tenders.length) { + final tender = tenders[index]; + return TenderCard( + date: tender.createdAt!.toString(), + title: tender.tender!.title!, + description: tender.tender!.description!, + location: tender.tender!.countryCode!, + countryFlag: AssetsManager.seFlag, + status: tender.status, + countryCode: tender.tender!.countryCode!, + projectStatus: tender.tender!.status!, + isDesktop: widget.isDesktop, + backgroundColor: + tender.status == 'Won' + ? AppColors.green0 + : tender.status == 'submitted' + ? AppColors.grey0 + : AppColors.red0.withValues(alpha: 0.2), + borderColor: + tender.status == 'Won' + ? AppColors.green10 + : tender.status == 'submitted' + ? AppColors.grey30 + : AppColors.red0, + statusIcon: + tender.status == 'Won' + ? AssetsManager.tickCircle + : tender.status == 'submitted' + ? null + : AssetsManager.closeCircle, + statusCardColor: + tender.status == 'Won' + ? AppColors.green20 + : tender.status == 'submitted' + ? AppColors.green20 + : AppColors.red0, + statusTextColor: + tender.status == 'Won' + ? AppColors.green30 + : tender.status == 'submitted' + ? AppColors.green30 + : AppColors.red10, + ); + } else { + return Padding( + padding: EdgeInsets.symmetric(vertical: 20.0.h()), + child: Center( + child: SizedBox( + width: 24.0.w(), + height: 24.0.h(), + child: CircularProgressIndicator( + color: AppColors.secondary50, + strokeWidth: 2, + ), + ), + ), + ); + } }, ); + }, + ); } } diff --git a/pubspec.lock b/pubspec.lock index 25bc59f..821d175 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -245,10 +245,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -500,10 +500,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -937,10 +937,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: