feat: add AI recommendations flow for tenders

Implement company AI onboarding/recommendation models, services, repository, and tender UI integration with supporting tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
AmirReza Jamali
2026-06-16 11:47:52 +03:30
parent 83b32549ce
commit 50e4f43738
34 changed files with 2558 additions and 112 deletions
+58 -32
View File
@@ -11,8 +11,10 @@ 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';
@@ -28,6 +30,9 @@ class DesktopTendersPage extends StatefulWidget {
class _DesktopTendersPageState extends State<DesktopTendersPage> {
late final TendersViewModel viewModel;
/// 0 = All, 1 = Recommended.
int _selectedTab = 0;
@override
void initState() {
super.initState();
@@ -50,47 +55,34 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
@override
Widget build(BuildContext context) {
final userRole = context.watch<AuthViewModel>().userRole;
final bool isAllTab = _selectedTab == 0;
return Scaffold(
backgroundColor: AppColors.backgroundColor,
body: SafeArea(
child: Column(
children: [
const DesktopNavigationWidget(currentIndex: 1),
SizedBox(height: 48.0.h()),
if (userRole == Role.analyst) const _SearchBox(),
if (userRole == Role.analyst) const _ActionButtons(),
SizedBox(height: 24.0.h()),
SizedBox(
width: 740,
child: TendersTabBar(
selectedIndex: _selectedTab,
onChanged: (index) => setState(() => _selectedTab = index),
),
),
SizedBox(height: 24.0.h()),
if (isAllTab && userRole == Role.analyst) const _SearchBox(),
if (isAllTab && userRole == Role.analyst) const _ActionButtons(),
Expanded(
child: Consumer<TendersViewModel>(
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,
child:
isAllTab
? const _AllTendersTab()
: const Center(
child: SizedBox(
width: 740,
child: RecommendedTendersView(),
),
),
),
);
},
),
),
],
),
@@ -99,6 +91,40 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
}
}
class _AllTendersTab extends StatelessWidget {
const _AllTendersTab();
@override
Widget build(BuildContext context) {
return Consumer<TendersViewModel>(
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();