From 3dca9fd3834a47493f289d1c4438114e3c3c214e Mon Sep 17 00:00:00 2001 From: amirrezaghabeli Date: Mon, 18 Aug 2025 15:33:50 +0330 Subject: [PATCH 1/4] added feedback --- lib/data/repositories/tenders_repository.dart | 17 ++ .../feedback_response/feedback_response.dart | 2 +- .../feedback_response.freezed.dart | 66 +++-- .../feedback_response.g.dart | 6 +- lib/data/services/tenders_service.dart | 31 +++ lib/view_models/tenders_view_model.dart | 232 ++++++------------ .../tenders/widgets/main_tenders_slider.dart | 35 ++- .../widgets/tender_action_buttons_row.dart | 71 +++--- 8 files changed, 231 insertions(+), 229 deletions(-) diff --git a/lib/data/repositories/tenders_repository.dart b/lib/data/repositories/tenders_repository.dart index 31b803b..acc2a2c 100644 --- a/lib/data/repositories/tenders_repository.dart +++ b/lib/data/repositories/tenders_repository.dart @@ -1,4 +1,5 @@ import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart'; import 'package:tm_app/data/services/model/tenders/tender_dislike_response/tender_dislike_response.dart'; import 'package:tm_app/data/services/model/tenders/tender_like_response/tender_like_response.dart'; import 'package:tm_app/data/services/model/tenders/tender_reject_response/tender_reject_response.dart'; @@ -15,6 +16,22 @@ class TendersRepository { return _tendersService.getTenders(); } + Future> getTenderFeedback({ + required String tenderId, + }) async { + return _tendersService.getTenderFeedback(tenderId: tenderId); + } + + Future> feedback({ + required String tenderId, + required String feedbackType, + }) async { + return _tendersService.feedback( + tenderId: tenderId, + feedbackType: feedbackType, + ); + } + Future> likeTender() async { return _tendersService.likeTender(); } diff --git a/lib/data/services/model/feedback_response/feedback_response.dart b/lib/data/services/model/feedback_response/feedback_response.dart index e9b1af1..727a66f 100644 --- a/lib/data/services/model/feedback_response/feedback_response.dart +++ b/lib/data/services/model/feedback_response/feedback_response.dart @@ -13,7 +13,7 @@ part 'feedback_response.g.dart'; abstract class FeedbackResponse with _$FeedbackResponse { const factory FeedbackResponse({ @JsonKey(name: 'meta') required Meta? meta, - @JsonKey(name: 'data') required List? data, + @JsonKey(name: 'data') required FeedbackData? data, @JsonKey(name: 'error') required ErrorModel? error, required String? message, required bool? success, diff --git a/lib/data/services/model/feedback_response/feedback_response.freezed.dart b/lib/data/services/model/feedback_response/feedback_response.freezed.dart index 978135b..e90d163 100644 --- a/lib/data/services/model/feedback_response/feedback_response.freezed.dart +++ b/lib/data/services/model/feedback_response/feedback_response.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$FeedbackResponse { -@JsonKey(name: 'meta') Meta? get meta;@JsonKey(name: 'data') List? get data;@JsonKey(name: 'error') ErrorModel? get error; String? get message; bool? get success; +@JsonKey(name: 'meta') Meta? get meta;@JsonKey(name: 'data') FeedbackData? get data;@JsonKey(name: 'error') ErrorModel? get error; String? get message; bool? get success; /// Create a copy of FeedbackResponse /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,12 +28,12 @@ $FeedbackResponseCopyWith get copyWith => _$FeedbackResponseCo @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is FeedbackResponse&&(identical(other.meta, meta) || other.meta == meta)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is FeedbackResponse&&(identical(other.meta, meta) || other.meta == meta)&&(identical(other.data, data) || other.data == data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,meta,const DeepCollectionEquality().hash(data),error,message,success); +int get hashCode => Object.hash(runtimeType,meta,data,error,message,success); @override String toString() { @@ -48,11 +48,11 @@ abstract mixin class $FeedbackResponseCopyWith<$Res> { factory $FeedbackResponseCopyWith(FeedbackResponse value, $Res Function(FeedbackResponse) _then) = _$FeedbackResponseCopyWithImpl; @useResult $Res call({ -@JsonKey(name: 'meta') Meta? meta,@JsonKey(name: 'data') List? data,@JsonKey(name: 'error') ErrorModel? error, String? message, bool? success +@JsonKey(name: 'meta') Meta? meta,@JsonKey(name: 'data') FeedbackData? data,@JsonKey(name: 'error') ErrorModel? error, String? message, bool? success }); -$MetaCopyWith<$Res>? get meta;$ErrorModelCopyWith<$Res>? get error; +$MetaCopyWith<$Res>? get meta;$FeedbackDataCopyWith<$Res>? get data;$ErrorModelCopyWith<$Res>? get error; } /// @nodoc @@ -69,7 +69,7 @@ class _$FeedbackResponseCopyWithImpl<$Res> return _then(_self.copyWith( meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable as Meta?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable -as List?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as FeedbackData?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as ErrorModel?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable as String?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable as bool?, @@ -91,6 +91,18 @@ $MetaCopyWith<$Res>? get meta { /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') +$FeedbackDataCopyWith<$Res>? get data { + if (_self.data == null) { + return null; + } + + return $FeedbackDataCopyWith<$Res>(_self.data!, (value) { + return _then(_self.copyWith(data: value)); + }); +}/// Create a copy of FeedbackResponse +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') $ErrorModelCopyWith<$Res>? get error { if (_self.error == null) { return null; @@ -181,7 +193,7 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function(@JsonKey(name: 'meta') Meta? meta, @JsonKey(name: 'data') List? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function(@JsonKey(name: 'meta') Meta? meta, @JsonKey(name: 'data') FeedbackData? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _FeedbackResponse() when $default != null: return $default(_that.meta,_that.data,_that.error,_that.message,_that.success);case _: @@ -202,7 +214,7 @@ return $default(_that.meta,_that.data,_that.error,_that.message,_that.success);c /// } /// ``` -@optionalTypeArgs TResult when(TResult Function(@JsonKey(name: 'meta') Meta? meta, @JsonKey(name: 'data') List? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function(@JsonKey(name: 'meta') Meta? meta, @JsonKey(name: 'data') FeedbackData? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success) $default,) {final _that = this; switch (_that) { case _FeedbackResponse(): return $default(_that.meta,_that.data,_that.error,_that.message,_that.success);case _: @@ -222,7 +234,7 @@ return $default(_that.meta,_that.data,_that.error,_that.message,_that.success);c /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function(@JsonKey(name: 'meta') Meta? meta, @JsonKey(name: 'data') List? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function(@JsonKey(name: 'meta') Meta? meta, @JsonKey(name: 'data') FeedbackData? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success)? $default,) {final _that = this; switch (_that) { case _FeedbackResponse() when $default != null: return $default(_that.meta,_that.data,_that.error,_that.message,_that.success);case _: @@ -237,19 +249,11 @@ return $default(_that.meta,_that.data,_that.error,_that.message,_that.success);c @JsonSerializable() class _FeedbackResponse implements FeedbackResponse { - const _FeedbackResponse({@JsonKey(name: 'meta') required this.meta, @JsonKey(name: 'data') required final List? data, @JsonKey(name: 'error') required this.error, required this.message, required this.success}): _data = data; + const _FeedbackResponse({@JsonKey(name: 'meta') required this.meta, @JsonKey(name: 'data') required this.data, @JsonKey(name: 'error') required this.error, required this.message, required this.success}); factory _FeedbackResponse.fromJson(Map json) => _$FeedbackResponseFromJson(json); @override@JsonKey(name: 'meta') final Meta? meta; - final List? _data; -@override@JsonKey(name: 'data') List? get data { - final value = _data; - if (value == null) return null; - if (_data is EqualUnmodifiableListView) return _data; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); -} - +@override@JsonKey(name: 'data') final FeedbackData? data; @override@JsonKey(name: 'error') final ErrorModel? error; @override final String? message; @override final bool? success; @@ -267,12 +271,12 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _FeedbackResponse&&(identical(other.meta, meta) || other.meta == meta)&&const DeepCollectionEquality().equals(other._data, _data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _FeedbackResponse&&(identical(other.meta, meta) || other.meta == meta)&&(identical(other.data, data) || other.data == data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,meta,const DeepCollectionEquality().hash(_data),error,message,success); +int get hashCode => Object.hash(runtimeType,meta,data,error,message,success); @override String toString() { @@ -287,11 +291,11 @@ abstract mixin class _$FeedbackResponseCopyWith<$Res> implements $FeedbackRespon factory _$FeedbackResponseCopyWith(_FeedbackResponse value, $Res Function(_FeedbackResponse) _then) = __$FeedbackResponseCopyWithImpl; @override @useResult $Res call({ -@JsonKey(name: 'meta') Meta? meta,@JsonKey(name: 'data') List? data,@JsonKey(name: 'error') ErrorModel? error, String? message, bool? success +@JsonKey(name: 'meta') Meta? meta,@JsonKey(name: 'data') FeedbackData? data,@JsonKey(name: 'error') ErrorModel? error, String? message, bool? success }); -@override $MetaCopyWith<$Res>? get meta;@override $ErrorModelCopyWith<$Res>? get error; +@override $MetaCopyWith<$Res>? get meta;@override $FeedbackDataCopyWith<$Res>? get data;@override $ErrorModelCopyWith<$Res>? get error; } /// @nodoc @@ -307,8 +311,8 @@ class __$FeedbackResponseCopyWithImpl<$Res> @override @pragma('vm:prefer-inline') $Res call({Object? meta = freezed,Object? data = freezed,Object? error = freezed,Object? message = freezed,Object? success = freezed,}) { return _then(_FeedbackResponse( meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable -as Meta?,data: freezed == data ? _self._data : data // ignore: cast_nullable_to_non_nullable -as List?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as Meta?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable +as FeedbackData?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as ErrorModel?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable as String?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable as bool?, @@ -331,6 +335,18 @@ $MetaCopyWith<$Res>? get meta { /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') +$FeedbackDataCopyWith<$Res>? get data { + if (_self.data == null) { + return null; + } + + return $FeedbackDataCopyWith<$Res>(_self.data!, (value) { + return _then(_self.copyWith(data: value)); + }); +}/// Create a copy of FeedbackResponse +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') $ErrorModelCopyWith<$Res>? get error { if (_self.error == null) { return null; diff --git a/lib/data/services/model/feedback_response/feedback_response.g.dart b/lib/data/services/model/feedback_response/feedback_response.g.dart index 8fd6980..00738c5 100644 --- a/lib/data/services/model/feedback_response/feedback_response.g.dart +++ b/lib/data/services/model/feedback_response/feedback_response.g.dart @@ -13,9 +13,9 @@ _FeedbackResponse _$FeedbackResponseFromJson(Map json) => ? null : Meta.fromJson(json['meta'] as Map), data: - (json['data'] as List?) - ?.map((e) => FeedbackData.fromJson(e as Map)) - .toList(), + json['data'] == null + ? null + : FeedbackData.fromJson(json['data'] as Map), error: json['error'] == null ? null diff --git a/lib/data/services/tenders_service.dart b/lib/data/services/tenders_service.dart index e465212..43d9e8d 100644 --- a/lib/data/services/tenders_service.dart +++ b/lib/data/services/tenders_service.dart @@ -1,6 +1,9 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:tm_app/core/utils/logger.dart'; import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart'; import 'package:tm_app/data/services/model/tenders/tender_dislike_response/tender_dislike_response.dart'; import 'package:tm_app/data/services/model/tenders/tender_like_response/tender_like_response.dart'; import 'package:tm_app/data/services/model/tenders/tender_reject_response/tender_reject_response.dart'; @@ -23,6 +26,34 @@ class TendersService { return result; } + Future> getTenderFeedback({ + required String tenderId, + }) async { + final result = await _networkManager.makeRequest( + '/api/v1/feedback/tender/$tenderId', + method: 'GET', + (json) => FeedbackResponse.fromJson(json), + ); + return result; + } + + Future> feedback({ + required String tenderId, + required String feedbackType, + }) async { + final data = jsonEncode({ + 'feedback_type': feedbackType, + 'tender_id': tenderId, + }); + final result = await _networkManager.makeRequest( + '/api/v1/feedback', + method: 'POST', + (json) => FeedbackResponse.fromJson(json), + data: data, + ); + return result; + } + Future> likeTender() async { try { // final result = await _networkManager.makeRequest( diff --git a/lib/view_models/tenders_view_model.dart b/lib/view_models/tenders_view_model.dart index 709cd13..1c3d324 100644 --- a/lib/view_models/tenders_view_model.dart +++ b/lib/view_models/tenders_view_model.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:tm_app/data/repositories/tenders_repository.dart'; -import 'package:tm_app/data/services/model/tender/tender_model.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/tenders/tenders_response/tenders_response.dart'; import '../core/utils/result.dart'; @@ -18,13 +19,31 @@ class TendersViewModel with ChangeNotifier { bool _isLoading = false; String? _errorMessage; TendersResponse? _tendersResponse; + FeedbackData? _feedbackData; bool get isLoading => _isLoading; String? get errorMessage => _errorMessage; TendersResponse? get tendersResponse => _tendersResponse; + FeedbackData? get feedbackData => _feedbackData; + + List feedbacks = []; int currentPage = 1; + /// Get feedback for a specific tender + FeedbackData? getFeedbackForTender(String tenderId) { + try { + return feedbacks.firstWhere((f) => f.tender == tenderId); + } on Exception { + return null; + } + } + + /// Check if feedback exists for a specific tender + bool hasFeedbackForTender(String tenderId) { + return feedbacks.any((f) => f.tender == tenderId); + } + Future getTenders() async { _isLoading = true; _errorMessage = null; @@ -34,6 +53,7 @@ class TendersViewModel with ChangeNotifier { switch (result) { case Ok(): _tendersResponse = result.value; + getTenderFeedback(_tendersResponse!.data!.tenders!.first.id!); break; case Error(): _errorMessage = result.error.toString(); @@ -45,166 +65,64 @@ class TendersViewModel with ChangeNotifier { notifyListeners(); } - /// Updates a tender's state in the list - void _updateTenderState( - String tenderId, { - bool? liked, - bool? disliked, - bool? rejected, - bool? submitted, - }) { - // if (_tendersResponse?.tendersData?.tenders != null) { - // final updatedTenders = - // _tendersResponse!.tendersData!.tenders!.map((element) { - // if (element.tenderId == tenderId) { - // return element.copyWith( - // liked: liked, - // disliked: disliked, - // rejected: rejected, - // submitted: submitted, - // ); - // } - // return element; - // }).toList(); + Future getTenderFeedback(String tenderId) async { + // _isLoading = true; + _errorMessage = null; + notifyListeners(); - // _tendersResponse = _tendersResponse!.copyWith( - // tendersData: _tendersResponse!.tendersData!.copyWith( - // tenders: updatedTenders, - // ), - // ); - // } + final result = await _tendersRepository.getTenderFeedback( + tenderId: tenderId, + ); + switch (result) { + case Ok(): + _feedbackData = result.value.data; + // Add or update _feedbackData in feedbacks list by tender id + _addOrUpdateFunction(_feedbackData!); + break; + case Error(): + _errorMessage = result.error.toString(); + break; + } + // _isLoading = false; + notifyListeners(); + _errorMessage = null; + notifyListeners(); } - /// Sets a tender as liked - Future markLiked(TenderModel tender) async { - // _updateTenderState( - // tender.id!, - // liked: true, - // disliked: false, - // rejected: false, - // submitted: tender.submitted, - // ); - // notifyListeners(); - // final result = await _tendersRepository.likeTender(); - // switch (result) { - // case Ok(): - // break; - // case Error(): - // break; - // } + Future feedback({ + required String tenderId, + required String feedbackType, + }) async { + _errorMessage = null; + notifyListeners(); + + final result = await _tendersRepository.feedback( + tenderId: tenderId, + feedbackType: feedbackType, + ); + switch (result) { + case Ok(): + _feedbackData = result.value.data; + if (_feedbackData != null) { + _addOrUpdateFunction(_feedbackData!); + } + + break; + case Error(): + _errorMessage = result.error.toString(); + break; + } + notifyListeners(); + _errorMessage = null; + notifyListeners(); } - /// Sets a tender as disliked - Future markDisliked(TenderModel tender) async { - // _updateTenderState( - // tender.tenderId!, - // disliked: true, - // liked: false, - // rejected: tender.rejected, - // submitted: false, - // ); - // notifyListeners(); - // final result = await _tendersRepository.dislikeTender(); - // switch (result) { - // case Ok(): - // break; - // case Error(): - // break; - // } - } - - /// Sets a tender as rejected - Future markRejected(TenderModel tender) async { - // _updateTenderState( - // tender.tenderId!, - // rejected: true, - // liked: false, - // disliked: tender.disliked, - // submitted: false, - // ); - // notifyListeners(); - // final result = await _tendersRepository.rejectTender(); - // switch (result) { - // case Ok(): - // break; - // case Error(): - // break; - // } - } - - /// Sets a tender as submitted - Future markSubmitted(TenderModel tender) async { - // _updateTenderState( - // tender.tenderId!, - // submitted: true, - // liked: tender.liked, - // disliked: false, - // rejected: false, - // ); - // notifyListeners(); - // final result = await _tendersRepository.submitTender(); - // switch (result) { - // case Ok(): - // break; - // case Error(): - // break; - // } - } - - /// Clears a specific state for a tender - void clearTenderState( - TenderModel tenderModel, { - bool? isLiked, - bool? isDisliked, - bool? isRejected, - bool? isSubmitted, - }) { - // _updateTenderState( - // tenderModel.tenderId!, - // liked: isLiked ?? tenderModel.liked, - // disliked: isDisliked ?? tenderModel.disliked, - // rejected: isRejected ?? tenderModel.rejected, - // submitted: isSubmitted ?? tenderModel.submitted, - // ); - } - - /// Toggles like state for a tender - void toggleLike(TenderModel tender) { - // if (tender.liked == true) { - // clearTenderState(tender, isLiked: false); - // } else { - // markLiked(tender); - // } - // notifyListeners(); - } - - /// Toggles dislike state for a tender - void toggleDislike(TenderModel tender) { - // if (tender.disliked == true) { - // clearTenderState(tender, isDisliked: false); - // } else { - // markDisliked(tender); - // } - // notifyListeners(); - } - - /// Toggles reject state for a tender - void toggleReject(TenderModel tender) { - // if (tender.rejected == true) { - // clearTenderState(tender, isRejected: false); - // } else { - // markRejected(tender); - // } - // notifyListeners(); - } - - /// Toggles submit state for a tender - void toggleSubmit(TenderModel tender) { - // if (tender.submitted == true) { - // clearTenderState(tender, isSubmitted: false); - // } else { - // markSubmitted(tender); - // } - // notifyListeners(); + void _addOrUpdateFunction(FeedbackData feedbackData) { + final index = feedbacks.indexWhere((f) => f.tender == feedbackData.tender); + if (index != -1) { + feedbacks[index] = feedbackData; + } else { + feedbacks.add(feedbackData); + } } } diff --git a/lib/views/tenders/widgets/main_tenders_slider.dart b/lib/views/tenders/widgets/main_tenders_slider.dart index f655d88..e48270c 100644 --- a/lib/views/tenders/widgets/main_tenders_slider.dart +++ b/lib/views/tenders/widgets/main_tenders_slider.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:provider/provider.dart'; import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/data/services/model/tender_data/tender_data.dart'; +import 'package:tm_app/view_models/tenders_view_model.dart'; import '../../../core/constants/assets.dart'; import '../../../core/utils/size_config.dart'; @@ -26,29 +28,45 @@ class _MainTendersSliderState extends State { int currentPage = 1; void _goToPreviousPage() { + if (currentPage == 1) { + return; + } + pageController.previousPage( duration: Duration(milliseconds: 300), curve: Curves.easeInOutCubic, ); setState(() { - if (currentPage == 1) { - return; - } currentPage = currentPage - 1; }); + + // Fetch feedback for the previous tender if not already loaded + final viewModel = context.read(); + final previousTenderId = widget.tenders[currentPage - 2].id!; + if (!viewModel.hasFeedbackForTender(previousTenderId)) { + viewModel.getTenderFeedback(previousTenderId); + } } void _goToNextPage() { + if (currentPage == widget.tenders.length) { + return; + } + pageController.nextPage( duration: Duration(milliseconds: 300), curve: Curves.easeInOutCubic, ); setState(() { - if (currentPage == widget.tenders.length) { - return; - } currentPage = currentPage + 1; }); + + // Fetch feedback for the next tender if not already loaded + final viewModel = context.read(); + final nextTenderId = widget.tenders[currentPage].id!; + if (!viewModel.hasFeedbackForTender(nextTenderId)) { + viewModel.getTenderFeedback(nextTenderId); + } } @override @@ -76,6 +94,11 @@ class _MainTendersSliderState extends State { setState(() { currentPage = index + 1; }); + // Fetch feedback for the new tender if not already loaded + final viewModel = context.read(); + if (!viewModel.hasFeedbackForTender(widget.tenders[index].id!)) { + viewModel.getTenderFeedback(widget.tenders[index].id!); + } }, itemBuilder: (context, index) { final tender = widget.tenders[index]; diff --git a/lib/views/tenders/widgets/tender_action_buttons_row.dart b/lib/views/tenders/widgets/tender_action_buttons_row.dart index 5d760aa..77b2b0a 100644 --- a/lib/views/tenders/widgets/tender_action_buttons_row.dart +++ b/lib/views/tenders/widgets/tender_action_buttons_row.dart @@ -21,24 +21,27 @@ class TenderActionButtonsRow extends StatelessWidget { @override Widget build(BuildContext context) { - final viewModel = context.watch(); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(width: 16.0.w()), + return Consumer( + builder: (context, viewModel, child) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(width: 16.0.w()), - // Dislike button - _dislikeButton(viewModel), - SizedBox(width: 32.0.w()), - //reject button - _rejectButton(viewModel), - SizedBox(width: 32.0.w()), - //submit button - _submitButton(viewModel), - SizedBox(width: 32.0.w()), - // Like button - _likeButton(viewModel), - ], + // Dislike button + _dislikeButton(viewModel), + SizedBox(width: 32.0.w()), + //reject button + _rejectButton(viewModel), + SizedBox(width: 32.0.w()), + //submit button + _submitButton(viewModel), + SizedBox(width: 32.0.w()), + // Like button + _likeButton(viewModel), + ], + ); + }, ); } @@ -154,12 +157,16 @@ class TenderActionButtonsRow extends StatelessWidget { } Widget _dislikeButton(TendersViewModel viewModel) { + // Find feedback for this specific tender + final feedback = viewModel.getFeedbackForTender(tender.id!); + final isDisliked = feedback?.feedbackType == 'dislike'; + return Tooltip( message: AppStrings.dislike, child: InkWell( splashColor: Colors.transparent, onTap: () { - // viewModel.toggleDislike(tender); + viewModel.feedback(tenderId: tender.id!, feedbackType: 'dislike'); }, child: Column( children: [ @@ -169,10 +176,7 @@ class TenderActionButtonsRow extends StatelessWidget { fit: BoxFit.cover, AssetsManager.dislike, colorFilter: ColorFilter.mode( - // tender.disliked ?? false - // ? AppColors.errorColor - // : - AppColors.grey50, + isDisliked ? AppColors.errorColor : AppColors.grey50, BlendMode.srcATop, ), ), @@ -183,11 +187,7 @@ class TenderActionButtonsRow extends StatelessWidget { style: TextStyle( fontSize: 12.0.sp(), fontWeight: FontWeight.w400, - color: - // tender.disliked ?? false - // ? AppColors.errorColor - // : - AppColors.grey50, + color: isDisliked ? AppColors.errorColor : AppColors.grey50, ), ), ], @@ -197,12 +197,16 @@ class TenderActionButtonsRow extends StatelessWidget { } Widget _likeButton(TendersViewModel viewModel) { + // Find feedback for this specific tender + final feedback = viewModel.getFeedbackForTender(tender.id!); + final isLiked = feedback?.feedbackType == 'like'; + return Tooltip( message: AppStrings.like, child: InkWell( splashColor: Colors.transparent, onTap: () { - // viewModel.toggleLike(tender); + viewModel.feedback(tenderId: tender.id!, feedbackType: 'like'); }, child: Column( children: [ @@ -212,10 +216,7 @@ class TenderActionButtonsRow extends StatelessWidget { fit: BoxFit.cover, AssetsManager.like, colorFilter: ColorFilter.mode( - // tender.liked ?? false - // ? AppColors.successColor - // : - AppColors.grey50, + isLiked ? AppColors.successColor : AppColors.grey50, BlendMode.srcATop, ), ), @@ -226,11 +227,7 @@ class TenderActionButtonsRow extends StatelessWidget { style: TextStyle( fontSize: 12.0.sp(), fontWeight: FontWeight.w400, - color: - // tender.liked ?? false - // ? AppColors.successColor - // : - AppColors.grey50, + color: isLiked ? AppColors.successColor : AppColors.grey50, ), ), ], From 1135fdd2ba0672b18aca4a9386ec6bab54b396c6 Mon Sep 17 00:00:00 2001 From: amirrezaghabeli Date: Mon, 18 Aug 2025 15:48:33 +0330 Subject: [PATCH 2/4] fixed view model --- lib/view_models/tenders_view_model.dart | 197 ++++++++++++++++++++++-- 1 file changed, 186 insertions(+), 11 deletions(-) diff --git a/lib/view_models/tenders_view_model.dart b/lib/view_models/tenders_view_model.dart index 1c3d324..07b1f16 100644 --- a/lib/view_models/tenders_view_model.dart +++ b/lib/view_models/tenders_view_model.dart @@ -1,7 +1,10 @@ +import 'dart:developer'; + import 'package:flutter/material.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/tender_model.dart'; import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart'; import '../core/utils/result.dart'; @@ -34,7 +37,7 @@ class TendersViewModel with ChangeNotifier { FeedbackData? getFeedbackForTender(String tenderId) { try { return feedbacks.firstWhere((f) => f.tender == tenderId); - } on Exception { + } catch (e) { return null; } } @@ -77,7 +80,19 @@ class TendersViewModel with ChangeNotifier { case Ok(): _feedbackData = result.value.data; // Add or update _feedbackData in feedbacks list by tender id - _addOrUpdateFunction(_feedbackData!); + if (_feedbackData != null) { + final index = feedbacks.indexWhere( + (f) => f.tender == _feedbackData!.tender, + ); + if (index != -1) { + feedbacks[index] = _feedbackData!; + log('Updated feedback for tender: ${_feedbackData!.tender}'); + } else { + feedbacks.add(_feedbackData!); + log('Added new feedback for tender: ${_feedbackData!.tender}'); + } + log('Total feedbacks: ${feedbacks.length}'); + } break; case Error(): _errorMessage = result.error.toString(); @@ -104,9 +119,15 @@ class TendersViewModel with ChangeNotifier { case Ok(): _feedbackData = result.value.data; if (_feedbackData != null) { - _addOrUpdateFunction(_feedbackData!); + final index = feedbacks.indexWhere( + (f) => f.tender == _feedbackData!.tender, + ); + if (index != -1) { + feedbacks[index] = _feedbackData!; + } else { + feedbacks.add(_feedbackData!); + } } - break; case Error(): _errorMessage = result.error.toString(); @@ -117,12 +138,166 @@ class TendersViewModel with ChangeNotifier { notifyListeners(); } - void _addOrUpdateFunction(FeedbackData feedbackData) { - final index = feedbacks.indexWhere((f) => f.tender == feedbackData.tender); - if (index != -1) { - feedbacks[index] = feedbackData; - } else { - feedbacks.add(feedbackData); - } + /// Updates a tender's state in the list + void _updateTenderState( + String tenderId, { + bool? liked, + bool? disliked, + bool? rejected, + bool? submitted, + }) { + // if (_tendersResponse?.tendersData?.tenders != null) { + // final updatedTenders = + // _tendersResponse!.tendersData!.tenders!.map((element) { + // if (element.tenderId == tenderId) { + // return element.copyWith( + // liked: liked, + // disliked: disliked, + // rejected: rejected, + // submitted: submitted, + // ); + // } + // return element; + // }).toList(); + + // _tendersResponse = _tendersResponse!.copyWith( + // tendersData: _tendersResponse!.tendersData!.copyWith( + // tenders: updatedTenders, + // ), + // ); + // } + } + + /// Sets a tender as liked + Future markLiked(TenderModel tender) async { + // _updateTenderState( + // tender.id!, + // liked: true, + // disliked: false, + // rejected: false, + // submitted: tender.submitted, + // ); + // notifyListeners(); + // final result = await _tendersRepository.likeTender(); + // switch (result) { + // case Ok(): + // break; + // case Error(): + // break; + // } + } + + /// Sets a tender as disliked + Future markDisliked(TenderModel tender) async { + // _updateTenderState( + // tender.tenderId!, + // disliked: true, + // liked: false, + // rejected: tender.rejected, + // submitted: false, + // ); + // notifyListeners(); + // final result = await _tendersRepository.dislikeTender(); + // switch (result) { + // case Ok(): + // break; + // case Error(): + // break; + // } + } + + /// Sets a tender as rejected + Future markRejected(TenderModel tender) async { + // _updateTenderState( + // tender.tenderId!, + // rejected: true, + // liked: false, + // disliked: tender.disliked, + // submitted: false, + // ); + // notifyListeners(); + // final result = await _tendersRepository.rejectTender(); + // switch (result) { + // case Ok(): + // break; + // case Error(): + // break; + // } + } + + /// Sets a tender as submitted + Future markSubmitted(TenderModel tender) async { + // _updateTenderState( + // tender.tenderId!, + // submitted: true, + // liked: tender.liked, + // disliked: false, + // rejected: false, + // ); + // notifyListeners(); + // final result = await _tendersRepository.submitTender(); + // switch (result) { + // case Ok(): + // break; + // case Error(): + // break; + // } + } + + /// Clears a specific state for a tender + void clearTenderState( + TenderModel tenderModel, { + bool? isLiked, + bool? isDisliked, + bool? isRejected, + bool? isSubmitted, + }) { + // _updateTenderState( + // tenderModel.tenderId!, + // liked: isLiked ?? tenderModel.liked, + // disliked: isDisliked ?? tenderModel.disliked, + // rejected: isRejected ?? tenderModel.rejected, + // submitted: isSubmitted ?? tenderModel.submitted, + // ); + } + + /// Toggles like state for a tender + void toggleLike(TenderModel tender) { + // if (tender.liked == true) { + // clearTenderState(tender, isLiked: false); + // } else { + // markLiked(tender); + // } + // notifyListeners(); + } + + /// Toggles dislike state for a tender + void toggleDislike(TenderModel tender) { + // if (tender.disliked == true) { + // clearTenderState(tender, isDisliked: false); + // } else { + // markDisliked(tender); + // } + // notifyListeners(); + } + + /// Toggles reject state for a tender + void toggleReject(TenderModel tender) { + // if (tender.rejected == true) { + // clearTenderState(tender, isRejected: false); + // } else { + // markRejected(tender); + // } + // notifyListeners(); + } + + /// Toggles submit state for a tender + void toggleSubmit(TenderModel tender) { + // if (tender.submitted == true) { + // clearTenderState(tender, isSubmitted: false); + // } else { + // markSubmitted(tender); + // } + // notifyListeners(); } } From 0c1b27fd1eee993530652a0b8b4dec01fb77fb0f Mon Sep 17 00:00:00 2001 From: amirrezaghabeli Date: Mon, 18 Aug 2025 20:15:40 +0330 Subject: [PATCH 3/4] removed unused codes --- lib/data/repositories/tenders_repository.dart | 20 --- lib/data/services/tenders_service.dart | 100 ----------- lib/view_models/tenders_view_model.dart | 164 ------------------ 3 files changed, 284 deletions(-) diff --git a/lib/data/repositories/tenders_repository.dart b/lib/data/repositories/tenders_repository.dart index acc2a2c..b66ed4d 100644 --- a/lib/data/repositories/tenders_repository.dart +++ b/lib/data/repositories/tenders_repository.dart @@ -1,9 +1,5 @@ import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart'; -import 'package:tm_app/data/services/model/tenders/tender_dislike_response/tender_dislike_response.dart'; -import 'package:tm_app/data/services/model/tenders/tender_like_response/tender_like_response.dart'; -import 'package:tm_app/data/services/model/tenders/tender_reject_response/tender_reject_response.dart'; -import 'package:tm_app/data/services/model/tenders/tender_submit_response/tender_submit_response.dart'; import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart'; import 'package:tm_app/data/services/tenders_service.dart'; @@ -31,20 +27,4 @@ class TendersRepository { feedbackType: feedbackType, ); } - - Future> likeTender() async { - return _tendersService.likeTender(); - } - - Future> dislikeTender() async { - return _tendersService.dislikeTender(); - } - - Future> submitTender() async { - return _tendersService.submitTender(); - } - - Future> rejectTender() async { - return _tendersService.rejectTender(); - } } diff --git a/lib/data/services/tenders_service.dart b/lib/data/services/tenders_service.dart index 43d9e8d..22536df 100644 --- a/lib/data/services/tenders_service.dart +++ b/lib/data/services/tenders_service.dart @@ -1,13 +1,7 @@ import 'dart:convert'; -import 'package:dio/dio.dart'; -import 'package:tm_app/core/utils/logger.dart'; import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart'; -import 'package:tm_app/data/services/model/tenders/tender_dislike_response/tender_dislike_response.dart'; -import 'package:tm_app/data/services/model/tenders/tender_like_response/tender_like_response.dart'; -import 'package:tm_app/data/services/model/tenders/tender_reject_response/tender_reject_response.dart'; -import 'package:tm_app/data/services/model/tenders/tender_submit_response/tender_submit_response.dart'; import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart'; import '../../core/network/network_manager.dart'; @@ -53,98 +47,4 @@ class TendersService { ); return result; } - - Future> likeTender() async { - try { - // final result = await _networkManager.makeRequest( - // '/api/v1/tenders', - // (json) => TenderLikeResponse.fromJson(json), - // method: 'POST', - // ); - // await Future.delayed(Duration(seconds: 2)); - appLogger.info('like tender success'); - // TODO: will change when api is ready - return Result.ok( - TenderLikeResponse.fromJson({'message': 'Tender liked successfully'}), - ); - } on DioException catch (e) { - appLogger.error('like tender failed: $e'); - return Result.error(e); - } on Exception catch (e, stackTrace) { - appLogger.error('like tender failed: $e', stackTrace: stackTrace); - return Result.error(Exception(e)); - } - } - - Future> dislikeTender() async { - try { - // final result = await _networkManager.makeRequest( - // '/api/v1/tenders', - // (json) => TenderDislikeResponse.fromJson(json), - // method: 'POST', - // ); - // await Future.delayed(Duration(seconds: 2)); - appLogger.info('dislike tender success'); - // TODO: will change when api is ready - return Result.ok( - TenderDislikeResponse.fromJson({ - 'message': 'Tender disliked successfully', - }), - ); - } on DioException catch (e) { - appLogger.error('dislike tender failed: $e'); - return Result.error(e); - } on Exception catch (e, stackTrace) { - appLogger.error('dislike tender failed: $e', stackTrace: stackTrace); - return Result.error(Exception(e)); - } - } - - Future> submitTender() async { - try { - // final result = await _networkManager.makeRequest( - // '/api/v1/tenders', - // (json) => TenderSubmitResponse.fromJson(json), - // method: 'POST', - // ); - // await Future.delayed(Duration(seconds: 2)); - appLogger.info('submit tender success'); - // TODO: will change when api is ready - return Result.ok( - TenderSubmitResponse.fromJson({ - 'message': 'Tender submitted successfully', - }), - ); - } on DioException catch (e) { - appLogger.error('submit tender failed: $e'); - return Result.error(e); - } on Exception catch (e, stackTrace) { - appLogger.error('submit tender failed: $e', stackTrace: stackTrace); - return Result.error(Exception(e)); - } - } - - Future> rejectTender() async { - try { - // final result = await _networkManager.makeRequest( - // '/api/v1/tenders', - // (json) => TenderRejectResponse.fromJson(json), - // method: 'POST', - // ); - // await Future.delayed(Duration(seconds: 2)); - appLogger.info('reject tender success'); - // TODO: will change when api is ready - return Result.ok( - TenderRejectResponse.fromJson({ - 'message': 'Tender rejected successfully', - }), - ); - } on DioException catch (e) { - appLogger.error('reject tender failed: $e'); - return Result.error(e); - } on Exception catch (e, stackTrace) { - appLogger.error('reject tender failed: $e', stackTrace: stackTrace); - return Result.error(Exception(e)); - } - } } diff --git a/lib/view_models/tenders_view_model.dart b/lib/view_models/tenders_view_model.dart index 07b1f16..91d22ba 100644 --- a/lib/view_models/tenders_view_model.dart +++ b/lib/view_models/tenders_view_model.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.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/tender_model.dart'; import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart'; import '../core/utils/result.dart'; @@ -137,167 +136,4 @@ class TendersViewModel with ChangeNotifier { _errorMessage = null; notifyListeners(); } - - /// Updates a tender's state in the list - void _updateTenderState( - String tenderId, { - bool? liked, - bool? disliked, - bool? rejected, - bool? submitted, - }) { - // if (_tendersResponse?.tendersData?.tenders != null) { - // final updatedTenders = - // _tendersResponse!.tendersData!.tenders!.map((element) { - // if (element.tenderId == tenderId) { - // return element.copyWith( - // liked: liked, - // disliked: disliked, - // rejected: rejected, - // submitted: submitted, - // ); - // } - // return element; - // }).toList(); - - // _tendersResponse = _tendersResponse!.copyWith( - // tendersData: _tendersResponse!.tendersData!.copyWith( - // tenders: updatedTenders, - // ), - // ); - // } - } - - /// Sets a tender as liked - Future markLiked(TenderModel tender) async { - // _updateTenderState( - // tender.id!, - // liked: true, - // disliked: false, - // rejected: false, - // submitted: tender.submitted, - // ); - // notifyListeners(); - // final result = await _tendersRepository.likeTender(); - // switch (result) { - // case Ok(): - // break; - // case Error(): - // break; - // } - } - - /// Sets a tender as disliked - Future markDisliked(TenderModel tender) async { - // _updateTenderState( - // tender.tenderId!, - // disliked: true, - // liked: false, - // rejected: tender.rejected, - // submitted: false, - // ); - // notifyListeners(); - // final result = await _tendersRepository.dislikeTender(); - // switch (result) { - // case Ok(): - // break; - // case Error(): - // break; - // } - } - - /// Sets a tender as rejected - Future markRejected(TenderModel tender) async { - // _updateTenderState( - // tender.tenderId!, - // rejected: true, - // liked: false, - // disliked: tender.disliked, - // submitted: false, - // ); - // notifyListeners(); - // final result = await _tendersRepository.rejectTender(); - // switch (result) { - // case Ok(): - // break; - // case Error(): - // break; - // } - } - - /// Sets a tender as submitted - Future markSubmitted(TenderModel tender) async { - // _updateTenderState( - // tender.tenderId!, - // submitted: true, - // liked: tender.liked, - // disliked: false, - // rejected: false, - // ); - // notifyListeners(); - // final result = await _tendersRepository.submitTender(); - // switch (result) { - // case Ok(): - // break; - // case Error(): - // break; - // } - } - - /// Clears a specific state for a tender - void clearTenderState( - TenderModel tenderModel, { - bool? isLiked, - bool? isDisliked, - bool? isRejected, - bool? isSubmitted, - }) { - // _updateTenderState( - // tenderModel.tenderId!, - // liked: isLiked ?? tenderModel.liked, - // disliked: isDisliked ?? tenderModel.disliked, - // rejected: isRejected ?? tenderModel.rejected, - // submitted: isSubmitted ?? tenderModel.submitted, - // ); - } - - /// Toggles like state for a tender - void toggleLike(TenderModel tender) { - // if (tender.liked == true) { - // clearTenderState(tender, isLiked: false); - // } else { - // markLiked(tender); - // } - // notifyListeners(); - } - - /// Toggles dislike state for a tender - void toggleDislike(TenderModel tender) { - // if (tender.disliked == true) { - // clearTenderState(tender, isDisliked: false); - // } else { - // markDisliked(tender); - // } - // notifyListeners(); - } - - /// Toggles reject state for a tender - void toggleReject(TenderModel tender) { - // if (tender.rejected == true) { - // clearTenderState(tender, isRejected: false); - // } else { - // markRejected(tender); - // } - // notifyListeners(); - } - - /// Toggles submit state for a tender - void toggleSubmit(TenderModel tender) { - // if (tender.submitted == true) { - // clearTenderState(tender, isSubmitted: false); - // } else { - // markSubmitted(tender); - // } - // notifyListeners(); - } } From da6b1f28cf83a026438f283336760cd3ff4a3889 Mon Sep 17 00:00:00 2001 From: amirrezaghabeli Date: Tue, 19 Aug 2025 08:08:31 +0330 Subject: [PATCH 4/4] tenders pagination added --- lib/data/repositories/tenders_repository.dart | 7 +- lib/data/services/tenders_service.dart | 7 +- lib/view_models/tenders_view_model.dart | 70 ++++++++++++++++++- .../tenders/widgets/main_tenders_slider.dart | 40 +++++++++-- 4 files changed, 111 insertions(+), 13 deletions(-) diff --git a/lib/data/repositories/tenders_repository.dart b/lib/data/repositories/tenders_repository.dart index b66ed4d..6d6f5bd 100644 --- a/lib/data/repositories/tenders_repository.dart +++ b/lib/data/repositories/tenders_repository.dart @@ -8,8 +8,11 @@ class TendersRepository { : _tendersService = tendersService; final TendersService _tendersService; - Future> getTenders() async { - return _tendersService.getTenders(); + Future> getTenders({ + required int limit, + required int offset, + }) async { + return _tendersService.getTenders(limit: limit, offset: offset); } Future> getTenderFeedback({ diff --git a/lib/data/services/tenders_service.dart b/lib/data/services/tenders_service.dart index 22536df..e7110c6 100644 --- a/lib/data/services/tenders_service.dart +++ b/lib/data/services/tenders_service.dart @@ -11,9 +11,12 @@ class TendersService { : _networkManager = networkManager; final NetworkManager _networkManager; - Future> getTenders() async { + Future> getTenders({ + required int limit, + required int offset, + }) async { final result = await _networkManager.makeRequest( - '/api/v1/tenders', + '/api/v1/tenders?limit=$limit&offset=$offset', method: 'GET', (json) => TendersResponse.fromJson(json), ); diff --git a/lib/view_models/tenders_view_model.dart b/lib/view_models/tenders_view_model.dart index 91d22ba..e245562 100644 --- a/lib/view_models/tenders_view_model.dart +++ b/lib/view_models/tenders_view_model.dart @@ -19,11 +19,13 @@ class TendersViewModel with ChangeNotifier { } bool _isLoading = false; + bool _isLoadingMore = false; String? _errorMessage; TendersResponse? _tendersResponse; FeedbackData? _feedbackData; bool get isLoading => _isLoading; + bool get isLoadingMore => _isLoadingMore; String? get errorMessage => _errorMessage; TendersResponse? get tendersResponse => _tendersResponse; FeedbackData? get feedbackData => _feedbackData; @@ -31,6 +33,11 @@ class TendersViewModel with ChangeNotifier { List feedbacks = []; int currentPage = 1; + int _currentOffset = 0; + static const int _pageSize = 10; + bool _hasMoreData = true; + + bool get hasMoreData => _hasMoreData; /// Get feedback for a specific tender FeedbackData? getFeedbackForTender(String tenderId) { @@ -49,13 +56,24 @@ class TendersViewModel with ChangeNotifier { Future getTenders() async { _isLoading = true; _errorMessage = null; + _currentOffset = 0; + _hasMoreData = true; notifyListeners(); - final result = await _tendersRepository.getTenders(); + final result = await _tendersRepository.getTenders( + limit: _pageSize, + offset: _currentOffset, + ); switch (result) { case Ok(): _tendersResponse = result.value; - getTenderFeedback(_tendersResponse!.data!.tenders!.first.id!); + // Check if we have more data + if (_tendersResponse?.data?.tenders != null) { + _hasMoreData = _tendersResponse!.data!.tenders!.length >= _pageSize; + } + if (_tendersResponse?.data?.tenders?.isNotEmpty == true) { + getTenderFeedback(_tendersResponse!.data!.tenders!.first.id!); + } break; case Error(): _errorMessage = result.error.toString(); @@ -67,6 +85,54 @@ class TendersViewModel with ChangeNotifier { notifyListeners(); } + Future loadMoreTenders() async { + if (_isLoadingMore || !_hasMoreData) return; + + _isLoadingMore = true; + _errorMessage = null; + notifyListeners(); + + _currentOffset += _pageSize; + final result = await _tendersRepository.getTenders( + limit: _pageSize, + offset: _currentOffset, + ); + + switch (result) { + case Ok(): + 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; + } + // Check if we have more data + _hasMoreData = result.value.data!.tenders!.length >= _pageSize; + } else { + _hasMoreData = false; + } + break; + case Error(): + _errorMessage = result.error.toString(); + _currentOffset -= _pageSize; // Revert offset on error + break; + } + + _isLoadingMore = false; + notifyListeners(); + _errorMessage = null; + notifyListeners(); + } + Future getTenderFeedback(String tenderId) async { // _isLoading = true; _errorMessage = null; diff --git a/lib/views/tenders/widgets/main_tenders_slider.dart b/lib/views/tenders/widgets/main_tenders_slider.dart index e48270c..5fe4b0b 100644 --- a/lib/views/tenders/widgets/main_tenders_slider.dart +++ b/lib/views/tenders/widgets/main_tenders_slider.dart @@ -94,11 +94,19 @@ class _MainTendersSliderState extends State { setState(() { currentPage = index + 1; }); + // Fetch feedback for the new tender if not already loaded final viewModel = context.read(); if (!viewModel.hasFeedbackForTender(widget.tenders[index].id!)) { viewModel.getTenderFeedback(widget.tenders[index].id!); } + + // Check if we need to load more tenders (when user reaches near the end) + if (index >= widget.tenders.length - 1 && + viewModel.hasMoreData && + !viewModel.isLoadingMore) { + viewModel.loadMoreTenders(); + } }, itemBuilder: (context, index) { final tender = widget.tenders[index]; @@ -142,13 +150,31 @@ class _MainTendersSliderState extends State { } Widget _pageNumberText() { - return Text( - '$currentPage/${widget.tenders.length}', - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.grey, - ), + final viewModel = context.watch(); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '$currentPage/${widget.tenders.length}', + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.grey, + ), + ), + if (viewModel.isLoadingMore) + Padding( + padding: EdgeInsets.only(top: 4.0.h()), + child: SizedBox( + width: 16.0.w(), + height: 16.0.w(), + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: AppColors.jellyBean, + ), + ), + ), + ], ); }