From 6cd8d7cc7f40e2eb7f5f635a21fa173b320fb845 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Mon, 29 Jun 2026 13:27:21 +0330 Subject: [PATCH] Update tenders recommendations view --- .../recommended_tenders_response.dart | 15 +- .../recommended_tenders_response.freezed.dart | 42 +-- .../recommended_tenders_response.g.dart | 4 +- lib/views/tenders/pages/d_tenders_page.dart | 249 ++---------------- lib/views/tenders/pages/m_tenders_page.dart | 81 +----- lib/views/tenders/pages/t_tenders_page.dart | 65 +---- .../widgets/recommended_tenders_view.dart | 91 ++----- .../tenders/widgets/tenders_tab_bar.dart | 8 +- .../ai_recommendations_view_model_test.dart | 61 +++-- 9 files changed, 124 insertions(+), 492 deletions(-) diff --git a/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.dart b/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.dart index 20e79d5..3cfe1bc 100644 --- a/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.dart +++ b/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.dart @@ -10,16 +10,13 @@ part 'recommended_tenders_response.g.dart'; @freezed abstract class RecommendedTendersResponse with _$RecommendedTendersResponse { @JsonSerializable(explicitToJson: true) - const factory -RecommendedTendersResponse({ - required bool? succes, - required String? message, - required ErrorModel? error, - required RecommendedTendersData? data, - + const factory RecommendedTendersResponse({ + @JsonKey(name: 'success') required bool? success, + required String? message, + required ErrorModel? error, + required RecommendedTendersData? data, }) = _RecommendedTendersResponse; - factory -RecommendedTendersResponse.fromJson(Map json) => + factory RecommendedTendersResponse.fromJson(Map json) => _$RecommendedTendersResponseFromJson(json); } diff --git a/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.freezed.dart b/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.freezed.dart index 02b0f29..ef341b3 100644 --- a/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.freezed.dart +++ b/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$RecommendedTendersResponse { - bool? get succes; String? get message; ErrorModel? get error; RecommendedTendersData? get data; + bool? get success; String? get message; ErrorModel? get error; RecommendedTendersData? get data; /// Create a copy of RecommendedTendersResponse /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $RecommendedTendersResponseCopyWith get copyWith => @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is RecommendedTendersResponse&&(identical(other.succes, succes) || other.succes == succes)&&(identical(other.message, message) || other.message == message)&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is RecommendedTendersResponse&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,succes,message,error,data); +int get hashCode => Object.hash(runtimeType,success,message,error,data); @override String toString() { - return 'RecommendedTendersResponse(succes: $succes, message: $message, error: $error, data: $data)'; + return 'RecommendedTendersResponse(success: $success, message: $message, error: $error, data: $data)'; } @@ -48,7 +48,7 @@ abstract mixin class $RecommendedTendersResponseCopyWith<$Res> { factory $RecommendedTendersResponseCopyWith(RecommendedTendersResponse value, $Res Function(RecommendedTendersResponse) _then) = _$RecommendedTendersResponseCopyWithImpl; @useResult $Res call({ - bool? succes, String? message, ErrorModel? error, RecommendedTendersData? data + bool? success, String? message, ErrorModel? error, RecommendedTendersData? data }); @@ -65,9 +65,9 @@ class _$RecommendedTendersResponseCopyWithImpl<$Res> /// Create a copy of RecommendedTendersResponse /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? succes = freezed,Object? message = freezed,Object? error = freezed,Object? data = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? success = freezed,Object? message = freezed,Object? error = freezed,Object? data = freezed,}) { return _then(_self.copyWith( -succes: freezed == succes ? _self.succes : succes // ignore: cast_nullable_to_non_nullable +success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as ErrorModel?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable @@ -180,10 +180,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( bool? succes, String? message, ErrorModel? error, RecommendedTendersData? data)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( bool? success, String? message, ErrorModel? error, RecommendedTendersData? data)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _RecommendedTendersResponse() when $default != null: -return $default(_that.succes,_that.message,_that.error,_that.data);case _: +return $default(_that.success,_that.message,_that.error,_that.data);case _: return orElse(); } @@ -201,10 +201,10 @@ return $default(_that.succes,_that.message,_that.error,_that.data);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( bool? succes, String? message, ErrorModel? error, RecommendedTendersData? data) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( bool? success, String? message, ErrorModel? error, RecommendedTendersData? data) $default,) {final _that = this; switch (_that) { case _RecommendedTendersResponse(): -return $default(_that.succes,_that.message,_that.error,_that.data);case _: +return $default(_that.success,_that.message,_that.error,_that.data);case _: throw StateError('Unexpected subclass'); } @@ -221,10 +221,10 @@ return $default(_that.succes,_that.message,_that.error,_that.data);case _: /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool? succes, String? message, ErrorModel? error, RecommendedTendersData? data)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool? success, String? message, ErrorModel? error, RecommendedTendersData? data)? $default,) {final _that = this; switch (_that) { case _RecommendedTendersResponse() when $default != null: -return $default(_that.succes,_that.message,_that.error,_that.data);case _: +return $default(_that.success,_that.message,_that.error,_that.data);case _: return null; } @@ -236,10 +236,10 @@ return $default(_that.succes,_that.message,_that.error,_that.data);case _: @JsonSerializable(explicitToJson: true) class _RecommendedTendersResponse implements RecommendedTendersResponse { - const _RecommendedTendersResponse({required this.succes, required this.message, required this.error, required this.data}); + const _RecommendedTendersResponse({required this.success, required this.message, required this.error, required this.data}); factory _RecommendedTendersResponse.fromJson(Map json) => _$RecommendedTendersResponseFromJson(json); -@override final bool? succes; +@override final bool? success; @override final String? message; @override final ErrorModel? error; @override final RecommendedTendersData? data; @@ -257,16 +257,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _RecommendedTendersResponse&&(identical(other.succes, succes) || other.succes == succes)&&(identical(other.message, message) || other.message == message)&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _RecommendedTendersResponse&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,succes,message,error,data); +int get hashCode => Object.hash(runtimeType,success,message,error,data); @override String toString() { - return 'RecommendedTendersResponse(succes: $succes, message: $message, error: $error, data: $data)'; + return 'RecommendedTendersResponse(success: $success, message: $message, error: $error, data: $data)'; } @@ -277,7 +277,7 @@ abstract mixin class _$RecommendedTendersResponseCopyWith<$Res> implements $Reco factory _$RecommendedTendersResponseCopyWith(_RecommendedTendersResponse value, $Res Function(_RecommendedTendersResponse) _then) = __$RecommendedTendersResponseCopyWithImpl; @override @useResult $Res call({ - bool? succes, String? message, ErrorModel? error, RecommendedTendersData? data + bool? success, String? message, ErrorModel? error, RecommendedTendersData? data }); @@ -294,9 +294,9 @@ class __$RecommendedTendersResponseCopyWithImpl<$Res> /// Create a copy of RecommendedTendersResponse /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? succes = freezed,Object? message = freezed,Object? error = freezed,Object? data = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? success = freezed,Object? message = freezed,Object? error = freezed,Object? data = freezed,}) { return _then(_RecommendedTendersResponse( -succes: freezed == succes ? _self.succes : succes // ignore: cast_nullable_to_non_nullable +success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as ErrorModel?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable diff --git a/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.g.dart b/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.g.dart index 20c1d78..756978b 100644 --- a/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.g.dart +++ b/lib/data/services/model/recommended_tenders_response/recommended_tenders_response.g.dart @@ -9,7 +9,7 @@ part of 'recommended_tenders_response.dart'; _RecommendedTendersResponse _$RecommendedTendersResponseFromJson( Map json, ) => _RecommendedTendersResponse( - succes: json['succes'] as bool?, + success: json['success'] as bool?, message: json['message'] as String?, error: json['error'] == null @@ -26,7 +26,7 @@ _RecommendedTendersResponse _$RecommendedTendersResponseFromJson( Map _$RecommendedTendersResponseToJson( _RecommendedTendersResponse instance, ) => { - 'succes': instance.succes, + 'success': instance.success, 'message': instance.message, 'error': instance.error?.toJson(), 'data': instance.data?.toJson(), diff --git a/lib/views/tenders/pages/d_tenders_page.dart b/lib/views/tenders/pages/d_tenders_page.dart index 3131781..f787a61 100644 --- a/lib/views/tenders/pages/d_tenders_page.dart +++ b/lib/views/tenders/pages/d_tenders_page.dart @@ -1,24 +1,12 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.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/tenders_view_model.dart'; -import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/shared/desktop_navigation_widget.dart'; -import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; -import 'package:tm_app/views/tenders/widgets/main_tenders_slider.dart'; import 'package:tm_app/views/tenders/widgets/recommended_tenders_view.dart'; -import 'package:tm_app/views/tenders/widgets/tenders_filter_dialog.dart'; -import 'package:tm_app/views/tenders/widgets/tenders_sort_dialog.dart'; -import 'package:tm_app/views/tenders/widgets/tenders_tab_bar.dart'; -import '../../../core/constants/common_strings.dart'; import '../../../core/utils/app_toast.dart'; -import '../../../view_models/auth_view_model.dart'; class DesktopTendersPage extends StatefulWidget { const DesktopTendersPage({required this.initialSelectedTab, super.key}); @@ -32,25 +20,13 @@ class DesktopTendersPage extends StatefulWidget { class _DesktopTendersPageState extends State { late final TendersViewModel viewModel; - /// 0 = All, 1 = Recommended. - int _selectedTab = 0; - @override void initState() { super.initState(); - _selectedTab = widget.initialSelectedTab; viewModel = context.read(); viewModel.addListener(_viewModelListener); } - @override - void didUpdateWidget(covariant DesktopTendersPage oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.initialSelectedTab != widget.initialSelectedTab) { - _selectedTab = widget.initialSelectedTab; - } - } - void _viewModelListener() { if (viewModel.feedbackErrorMessage != null) { AppToast.error(context, viewModel.feedbackErrorMessage!); @@ -65,8 +41,6 @@ class _DesktopTendersPageState extends State { @override Widget build(BuildContext context) { - final userRole = context.watch().userRole; - final bool isAllTab = _selectedTab == 0; return Scaffold( backgroundColor: AppColors.backgroundColor, body: SafeArea( @@ -74,216 +48,29 @@ class _DesktopTendersPageState extends State { children: [ const DesktopNavigationWidget(currentIndex: 1), SizedBox(height: 24.0.h()), - SizedBox( - width: 740, - child: TendersTabBar( - selectedIndex: _selectedTab, - onChanged: (index) => setState(() => _selectedTab = index), + // Tab bar hidden while tenders only shows recommendations. + // SizedBox( + // width: 740, + // child: TendersTabBar( + // selectedIndex: 1, + // onChanged: (index) {}, + // ), + // ), + // SizedBox(height: 24.0.h()), + // All tenders controls are hidden while the All tab is disabled. + // if (userRole == Role.analyst) const _SearchBox(), + // if (userRole == Role.analyst) const _ActionButtons(), + const Expanded( + child: Center( + child: SizedBox( + width: 740, + child: RecommendedTendersView(isDesktop: true), + ), ), ), - SizedBox(height: 24.0.h()), - if (isAllTab && userRole == Role.analyst) const _SearchBox(), - if (isAllTab && userRole == Role.analyst) const _ActionButtons(), - Expanded( - child: - isAllTab - ? const _AllTendersTab() - : const Center( - child: SizedBox( - width: 740, - child: RecommendedTendersView(), - ), - ), - ), ], ), ), ); } } - -class _AllTendersTab extends StatelessWidget { - const _AllTendersTab(); - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, vm, child) { - if (vm.isLoading) { - return const Center( - child: CircularProgressIndicator(color: AppColors.jellyBean), - ); - } - if (vm.errorMessage != null) { - return Center(child: Text(vm.errorMessage!)); - } - final tenders = vm.tendersResponse?.data?.tenders ?? []; - if (tenders.isEmpty) { - return const Center(child: Text(CommonStrings.noData)); - } - - return Center( - child: SizedBox( - width: 740, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 4.0.h()), - child: MainTendersSlider(tenders: tenders, isDesktop: true), - ), - ), - ); - }, - ); - } -} - -class _SearchBox extends StatefulWidget { - const _SearchBox(); - - @override - State<_SearchBox> createState() => _SearchBoxState(); -} - -class _SearchBoxState extends State<_SearchBox> { - final TextEditingController _searchController = TextEditingController(); - Timer? _debounceTimer; - - @override - void dispose() { - _searchController.dispose(); - _debounceTimer?.cancel(); - super.dispose(); - } - - void _onSearchChanged(String query) { - _debounceTimer?.cancel(); - _debounceTimer = Timer(const Duration(milliseconds: 500), () { - final viewModel = context.read(); - viewModel.searchTenders(query); - }); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 48.0.h(), - width: 680, - child: TextField( - controller: _searchController, - onChanged: _onSearchChanged, - style: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey60), - decoration: InputDecoration( - hintText: TendersStrings.search, - hintStyle: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey60), - prefixIcon: Padding( - padding: EdgeInsets.all(12.0.w()), - child: SvgPicture.asset( - AssetsManager.normalSearch, - width: 24.0.w(), - height: 24.0.h(), - ), - ), - filled: true, - fillColor: AppColors.grey0, - contentPadding: EdgeInsets.symmetric( - horizontal: 12.0.w(), - vertical: 12.0.h(), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12.0.w()), - borderSide: const BorderSide(color: AppColors.mainBlue, width: 1), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12.0.w()), - borderSide: BorderSide(color: AppColors.grey40, width: 1), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12.0.w()), - borderSide: const BorderSide(color: AppColors.grey, width: 1.5), - ), - ), - ), - ); - } -} - -class _ActionButtons extends StatelessWidget { - const _ActionButtons(); - - @override - Widget build(BuildContext context) { - final viewModel = context.read(); - return Container( - margin: EdgeInsets.only(top: 25.0.h()), - height: 40.0.h(), - width: 680, - child: Row( - children: [ - BaseButton( - width: 178.0.w(), - isEnabled: true, - onPressed: () { - showDialog( - context: context, - builder: - (_) => Dialog( - backgroundColor: AppColors.backgroundColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: SizedBox( - width: 560, - child: TendersFilterDialog( - viewModel: viewModel, - onFilterChanged: (dateRange, status) { - viewModel.setDateRange(dateRange); - viewModel.setStatus(status!); - viewModel.callWithSortAndStatus(); - }, - ), - ), - ), - ); - }, - text: TendersStrings.filter, - icon: AssetsManager.filter, - iconColor: AppColors.textBlue, - backgroundColor: AppColors.primary20, - textColor: AppColors.textBlue, - ), - SizedBox(width: 10.0.w()), - BaseButton( - width: 178.0.w(), - isEnabled: true, - onPressed: () { - showDialog( - context: context, - builder: - (_) => Dialog( - backgroundColor: AppColors.backgroundColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: SizedBox( - width: 560, - child: TendersSortDialog( - viewModel: viewModel, - onConfirm: (sort) { - viewModel.setSort(sort); - viewModel.callWithSortAndStatus(); - }, - ), - ), - ), - ); - }, - text: TendersStrings.sort, - icon: AssetsManager.sort, - iconColor: AppColors.textBlue, - backgroundColor: AppColors.primary20, - textColor: AppColors.textBlue, - ), - ], - ), - ); - } -} diff --git a/lib/views/tenders/pages/m_tenders_page.dart b/lib/views/tenders/pages/m_tenders_page.dart index a710f14..0b9bf84 100644 --- a/lib/views/tenders/pages/m_tenders_page.dart +++ b/lib/views/tenders/pages/m_tenders_page.dart @@ -1,16 +1,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/view_models/tenders_view_model.dart'; -import '../../../core/constants/common_strings.dart'; import '../../../core/theme/colors.dart'; import '../../../core/utils/app_toast.dart'; import '../../shared/tender_app_bar.dart'; import '../strings/tenders_strings.dart'; -import '../widgets/main_tenders_slider.dart'; import '../widgets/recommended_tenders_view.dart'; -import '../widgets/tenders_tab_bar.dart'; class MobileTendersPage extends StatefulWidget { const MobileTendersPage({required this.initialSelectedTab, super.key}); @@ -24,25 +20,13 @@ class MobileTendersPage extends StatefulWidget { class _MobileTendersPageState extends State { late final TendersViewModel viewModel; - /// 0 = All, 1 = Recommended. - int _selectedTab = 0; - @override void initState() { super.initState(); - _selectedTab = widget.initialSelectedTab; viewModel = context.read(); viewModel.addListener(_viewModelListener); } - @override - void didUpdateWidget(covariant MobileTendersPage oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.initialSelectedTab != widget.initialSelectedTab) { - _selectedTab = widget.initialSelectedTab; - } - } - void _viewModelListener() { if (viewModel.feedbackErrorMessage != null) { AppToast.error(context, viewModel.feedbackErrorMessage!); @@ -60,63 +44,24 @@ class _MobileTendersPageState extends State { return Scaffold( backgroundColor: AppColors.backgroundColor, appBar: tenderMobileAppBar(title: TendersStrings.tendersTitle), - body: SafeArea( + body: const SafeArea( child: Column( children: [ - Padding( - padding: EdgeInsets.symmetric( - horizontal: 24.0.w(), - vertical: 12.0.h(), - ), - child: TendersTabBar( - selectedIndex: _selectedTab, - onChanged: (index) => setState(() => _selectedTab = index), - ), - ), - Expanded( - child: - _selectedTab == 0 - ? const _AllTendersTab() - : const RecommendedTendersView(), - ), + // Tab bar hidden while tenders only shows recommendations. + // Padding( + // padding: EdgeInsets.symmetric( + // horizontal: 24.0.w(), + // vertical: 12.0.h(), + // ), + // child: TendersTabBar( + // selectedIndex: 1, + // onChanged: (index) {}, + // ), + // ), + Expanded(child: RecommendedTendersView()), ], ), ), ); } } - -class _AllTendersTab extends StatelessWidget { - const _AllTendersTab(); - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, viewModel, child) { - if (viewModel.isLoading) { - return const Center( - child: CircularProgressIndicator(color: AppColors.jellyBean), - ); - } - if (viewModel.errorMessage != null) { - return Center(child: Text(viewModel.errorMessage!)); - } - - if ((viewModel.tendersResponse?.data?.tenders == null || - viewModel.tendersResponse!.data!.tenders!.isEmpty) && - viewModel.isLoading == false) { - return const Center(child: Text(CommonStrings.noData)); - } - - return SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 18.0.h()), - child: MainTendersSlider( - tenders: viewModel.tendersResponse?.data?.tenders ?? [], - ), - ), - ); - }, - ); - } -} diff --git a/lib/views/tenders/pages/t_tenders_page.dart b/lib/views/tenders/pages/t_tenders_page.dart index 41f8e6b..61b6b81 100644 --- a/lib/views/tenders/pages/t_tenders_page.dart +++ b/lib/views/tenders/pages/t_tenders_page.dart @@ -4,13 +4,10 @@ import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/view_models/tenders_view_model.dart'; import 'package:tm_app/views/shared/tablet_navigation_widget.dart'; -import 'package:tm_app/views/tenders/widgets/main_tenders_slider.dart'; -import '../../../core/constants/common_strings.dart'; import '../../shared/tender_app_bar.dart'; import '../strings/tenders_strings.dart'; import '../widgets/recommended_tenders_view.dart'; -import '../widgets/tenders_tab_bar.dart'; class TabletTendersPage extends StatefulWidget { const TabletTendersPage({required this.initialSelectedTab, super.key}); @@ -24,24 +21,12 @@ class TabletTendersPage extends StatefulWidget { class _TabletTendersPageState extends State { late final TendersViewModel viewModel; - /// 0 = All, 1 = Recommended. - int _selectedTab = 0; - @override void initState() { super.initState(); - _selectedTab = widget.initialSelectedTab; viewModel = context.read(); } - @override - void didUpdateWidget(covariant TabletTendersPage oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.initialSelectedTab != widget.initialSelectedTab) { - _selectedTab = widget.initialSelectedTab; - } - } - @override Widget build(BuildContext context) { final GlobalKey key = GlobalKey(); @@ -56,52 +41,18 @@ class _TabletTendersPageState extends State { drawer: const TabletNavigationWidget(currentIndex: 1), body: Padding( padding: EdgeInsetsDirectional.only(end: 24.0.w(), top: 24.0.h()), - child: Column( + child: const Column( children: [ - TendersTabBar( - selectedIndex: _selectedTab, - onChanged: (index) => setState(() => _selectedTab = index), - ), - SizedBox(height: 24.0.h()), - Expanded( - child: - _selectedTab == 0 - ? const _AllTendersTab() - : const RecommendedTendersView(), - ), + // Tab bar hidden while tenders only shows recommendations. + // TendersTabBar( + // selectedIndex: 1, + // onChanged: (index) {}, + // ), + // SizedBox(height: 24.0.h()), + Expanded(child: RecommendedTendersView()), ], ), ), ); } } - -class _AllTendersTab extends StatelessWidget { - const _AllTendersTab(); - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, viewModel, child) { - if (viewModel.isLoading) { - return const Center( - child: CircularProgressIndicator(color: AppColors.jellyBean), - ); - } - if (viewModel.errorMessage != null) { - return Center(child: Text(viewModel.errorMessage!)); - } - if ((viewModel.tendersResponse?.data?.tenders == null || - viewModel.tendersResponse!.data!.tenders!.isEmpty) && - viewModel.isLoading == false) { - return const Center(child: Text(CommonStrings.noData)); - } - return SingleChildScrollView( - child: MainTendersSlider( - tenders: viewModel.tendersResponse?.data?.tenders ?? [], - ), - ); - }, - ); - } -} diff --git a/lib/views/tenders/widgets/recommended_tenders_view.dart b/lib/views/tenders/widgets/recommended_tenders_view.dart index f583320..aa18ca4 100644 --- a/lib/views/tenders/widgets/recommended_tenders_view.dart +++ b/lib/views/tenders/widgets/recommended_tenders_view.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:tm_app/core/routes/app_routes.dart'; import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/size_config.dart'; -import 'package:tm_app/data/services/model/tender_data/tender_data.dart'; import 'package:tm_app/view_models/ai_recommendations_view_model.dart'; import '../strings/tenders_strings.dart'; +import 'main_tenders_slider.dart'; /// The "Recommended" tab body. Handles loading, empty/"still learning", /// error+retry and the recommended tender list for [AiRecommendationsViewModel]. class RecommendedTendersView extends StatefulWidget { - const RecommendedTendersView({super.key}); + const RecommendedTendersView({super.key, this.isDesktop = false}); + + final bool isDesktop; @override State createState() => _RecommendedTendersViewState(); @@ -49,15 +50,20 @@ class _RecommendedTendersViewState extends State { ); } if (vm.recommendedTenders.isNotEmpty) { - return RefreshIndicator( - color: AppColors.jellyBean, - onRefresh: vm.refresh, - child: ListView.separated( - padding: EdgeInsets.all(16.0.h()), - itemCount: vm.recommendedTenders.length, - separatorBuilder: (_, __) => SizedBox(height: 12.0.h()), - itemBuilder: (context, index) => - _RecommendedTenderCard(tender: vm.recommendedTenders[index]), + if (widget.isDesktop) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 4.0.h()), + child: MainTendersSlider( + tenders: vm.recommendedTenders, + isDesktop: true, + ), + ); + } + + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 18.0.h()), + child: MainTendersSlider(tenders: vm.recommendedTenders), ), ); } @@ -71,67 +77,6 @@ class _RecommendedTendersViewState extends State { } } -class _RecommendedTenderCard extends StatelessWidget { - const _RecommendedTenderCard({required this.tender}); - - final TenderData tender; - - void _openDetail(BuildContext context) { - final id = tender.id; - if (id == null || id.isEmpty) { - return; - } - TenderDetailRouteData(tenderId: id).push(context); - } - - @override - Widget build(BuildContext context) { - final title = tender.title?.trim() ?? ''; - final buyer = tender.buyerOrganization?.name?.trim() ?? ''; - return InkWell( - borderRadius: BorderRadius.circular(12.0.w()), - onTap: () => _openDetail(context), - child: Container( - width: double.infinity, - padding: EdgeInsets.all(16.0.h()), - decoration: BoxDecoration( - color: AppColors.grey0, - borderRadius: BorderRadius.circular(12.0.w()), - border: Border.all(color: AppColors.grey30), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (title.isNotEmpty) - SelectableText( - title, - maxLines: 2, - style: TextStyle( - fontSize: 15.0.sp(), - fontWeight: FontWeight.w600, - color: AppColors.grey80, - ), - ), - if (buyer.isNotEmpty) ...[ - SizedBox(height: 8.0.h()), - Text( - buyer, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.0.sp(), - fontWeight: FontWeight.w400, - color: AppColors.grey60, - ), - ), - ], - ], - ), - ), - ); - } -} - /// Centered title/body message with an optional retry button. Wrapped in a /// scroll view so pull-to-refresh works even when empty. class _MessageState extends StatelessWidget { diff --git a/lib/views/tenders/widgets/tenders_tab_bar.dart b/lib/views/tenders/widgets/tenders_tab_bar.dart index c9e874b..d972100 100644 --- a/lib/views/tenders/widgets/tenders_tab_bar.dart +++ b/lib/views/tenders/widgets/tenders_tab_bar.dart @@ -4,8 +4,7 @@ import 'package:tm_app/core/utils/size_config.dart'; import '../strings/tenders_strings.dart'; -/// Segmented control switching between the "All" tenders list and the -/// "Recommended" AI tab. +/// Segmented control for the tenders list. class TendersTabBar extends StatelessWidget { const TendersTabBar({ required this.selectedIndex, @@ -13,7 +12,7 @@ class TendersTabBar extends StatelessWidget { super.key, }); - /// 0 = All, 1 = Recommended. + /// 1 = Recommended. The former All tab is intentionally hidden. final int selectedIndex; final ValueChanged onChanged; @@ -29,7 +28,8 @@ class TendersTabBar extends StatelessWidget { ), child: Row( children: [ - _segment(TendersStrings.allTab, 0), + // All tenders is hidden for now. + // _segment(TendersStrings.allTab, 0), _segment(TendersStrings.recommendedTab, 1), ], ), diff --git a/test/view_models/ai_recommendations_view_model_test.dart b/test/view_models/ai_recommendations_view_model_test.dart index 8e3b382..de95297 100644 --- a/test/view_models/ai_recommendations_view_model_test.dart +++ b/test/view_models/ai_recommendations_view_model_test.dart @@ -15,34 +15,39 @@ import 'package:tm_app/view_models/ai_recommendations_view_model.dart'; void main() { group('AiRecommendationsViewModel', () { - test('loads recommended tenders from GET /api/v1/tenders/recommend', () async { - final prefs = await _prefs(); - final homeRepository = _FakeHomeRepository( - prefs, - result: Result.ok( - RecommendedTendersResponse( - succes: true, - message: 'ok', - error: null, - data: RecommendedTendersData( - tenders: [_sampleTender(id: 'tender-1', title: 'Bridge repair')], - metadata: null, + test( + 'loads recommended tenders from GET /api/v1/tenders/recommend', + () async { + final prefs = await _prefs(); + final homeRepository = _FakeHomeRepository( + prefs, + result: Result.ok( + RecommendedTendersResponse( + success: true, + message: 'ok', + error: null, + data: RecommendedTendersData( + tenders: [ + _sampleTender(id: 'tender-1', title: 'Bridge repair'), + ], + metadata: null, + ), ), ), - ), - ); - final viewModel = AiRecommendationsViewModel( - companyAiRepository: _FakeCompanyAiRepository(prefs), - homeRepository: homeRepository, - ); + ); + final viewModel = AiRecommendationsViewModel( + companyAiRepository: _FakeCompanyAiRepository(prefs), + homeRepository: homeRepository, + ); - await viewModel.loadRecommendations(); + await viewModel.loadRecommendations(); - expect(viewModel.hasLoaded, isTrue); - expect(viewModel.errorMessage, isNull); - expect(homeRepository.recommendedTendersCalls, 1); - expect(viewModel.recommendedTenders.single.id, 'tender-1'); - }); + expect(viewModel.hasLoaded, isTrue); + expect(viewModel.errorMessage, isNull); + expect(homeRepository.recommendedTendersCalls, 1); + expect(viewModel.recommendedTenders.single.id, 'tender-1'); + }, + ); test('keeps failed transient loads retryable for loadIfNeeded', () async { final prefs = await _prefs(); @@ -52,11 +57,13 @@ void main() { Result.error(Exception('network failed')), Result.ok( RecommendedTendersResponse( - succes: true, + success: true, message: 'ok', error: null, data: RecommendedTendersData( - tenders: [_sampleTender(id: 'tender-1', title: 'Bridge repair')], + tenders: [ + _sampleTender(id: 'tender-1', title: 'Bridge repair'), + ], metadata: null, ), ), @@ -128,7 +135,7 @@ class _FakeHomeRepository extends HomeRepository { result ?? const Result.ok( RecommendedTendersResponse( - succes: true, + success: true, message: 'ok', error: null, data: RecommendedTendersData(tenders: [], metadata: null),