sort and filter added to tenders web

This commit is contained in:
amirrezaghabeli
2025-09-23 13:15:41 +03:30
parent 9efc7b7f1a
commit 801e170d7c
10 changed files with 190 additions and 28 deletions
@@ -30,7 +30,7 @@ class AppShellScreen extends StatelessWidget {
context.read<HomeViewModel>().init(); context.read<HomeViewModel>().init();
} }
if (index == 1) { if (index == 1) {
context.read<TendersViewModel>().getTenders(); context.read<TendersViewModel>().init(context);
} }
}); });
} }
@@ -15,11 +15,19 @@ class TendersRepository {
required int limit, required int limit,
required int offset, required int offset,
String? query, String? query,
String? sortBy,
String? sortOrder,
String? publicationDateFrom,
String? publicationDateTo,
}) async { }) async {
return _tendersService.getTenders( return _tendersService.getTenders(
limit: limit, limit: limit,
offset: offset, offset: offset,
query: query, query: query,
sortBy: sortBy,
sortOrder: sortOrder,
publicationDateFrom: publicationDateFrom,
publicationDateTo: publicationDateTo,
); );
} }
+19
View File
@@ -1,14 +1,33 @@
import 'package:tm_app/core/utils/logger.dart';
class TendersApi { class TendersApi {
static const String tenders = '/api/v1/tenders'; static const String tenders = '/api/v1/tenders';
static String getTenders({ static String getTenders({
required int limit, required int limit,
required int offset, required int offset,
String? query, String? query,
String? sortBy,
String? sortOrder,
String? publicationDateFrom,
String? publicationDateTo,
}) { }) {
String url = '$tenders?limit=$limit&offset=$offset'; String url = '$tenders?limit=$limit&offset=$offset';
if (query != null && query.isNotEmpty) { if (query != null && query.isNotEmpty) {
url += '&q=${Uri.encodeComponent(query)}'; url += '&q=${Uri.encodeComponent(query)}';
} }
if (sortBy != null && sortBy.isNotEmpty) {
url += '&sort_by=$sortBy';
}
if (sortOrder != null && sortOrder.isNotEmpty) {
url += '&sort_order=$sortOrder';
}
if (publicationDateFrom != null && publicationDateFrom.isNotEmpty) {
url += '&publication_date_from=$publicationDateFrom';
}
if (publicationDateTo != null && publicationDateTo.isNotEmpty) {
url += '&publication_date_to=$publicationDateTo';
}
AppLogger().logWarning('🔍 Tenders API: $url');
return url; return url;
} }
+14 -1
View File
@@ -18,9 +18,22 @@ class TendersService {
required int limit, required int limit,
required int offset, required int offset,
String? query, String? query,
String? sortBy,
String? sortOrder,
String? publicationDateFrom,
String? publicationDateTo,
}) async { }) async {
final result = await _networkManager.makeRequest( final result = await _networkManager.makeRequest(
TendersApi.getTenders(limit: limit, offset: offset, query: query), TendersApi.getTenders(
limit: limit,
offset: offset,
query: query,
sortBy: sortBy,
sortOrder: sortOrder,
publicationDateFrom: publicationDateFrom,
publicationDateTo: publicationDateTo,
),
method: 'GET', method: 'GET',
(json) => TendersResponse.fromJson(json), (json) => TendersResponse.fromJson(json),
); );
+131 -11
View File
@@ -1,11 +1,13 @@
import 'dart:developer'; import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tm_app/core/utils/device_utils.dart'; import 'package:tm_app/core/utils/device_utils.dart';
import 'package:tm_app/data/repositories/tenders_repository.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_data/feedback_data.dart';
import 'package:tm_app/data/services/model/feedback_response/feedback_response.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 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart';
import 'package:tm_app/views/tenders/strings/tenders_strings.dart';
import '../core/utils/result.dart'; import '../core/utils/result.dart';
import 'home_view_model.dart'; import 'home_view_model.dart';
@@ -47,6 +49,88 @@ class TendersViewModel with ChangeNotifier {
bool get hasMoreData => _hasMoreData; bool get hasMoreData => _hasMoreData;
// Filter States
DateTimeRange? selectedDateRange;
String selectedStatus = 'All';
String selectedSort = '';
String sortBy = '';
String sortOrder = '';
final _fmt = DateFormat('yyyy/MM/dd');
String get startDateFormatted =>
selectedDateRange?.start != null
? _fmt.format(selectedDateRange!.start)
: 'YYYY/MM/DD';
String get endDateFormatted =>
selectedDateRange?.end != null
? _fmt.format(selectedDateRange!.end)
: 'YYYY/MM/DD';
String get startDateUnix =>
selectedDateRange?.start != null
? (selectedDateRange!.start.millisecondsSinceEpoch ~/ 1000).toString()
: '';
String get endDateUnix =>
selectedDateRange?.end != null
? (selectedDateRange!.end.millisecondsSinceEpoch ~/ 1000).toString()
: '';
// --- Filter Actions ---
void setDateRange(DateTimeRange? range) {
selectedDateRange = range;
notifyListeners();
}
void setStatus(String status) {
// Clear data when status changes
_clearAllData();
selectedStatus = status;
currentPage = 1;
notifyListeners();
}
void setSort(String sort) {
// Clear data when status changes
_clearAllData();
selectedSort = sort;
if (sort == TendersStrings.lessBudget) {
sortBy = 'estimated_value';
sortOrder = 'asc';
} else if (sort == TendersStrings.moreBudget) {
sortBy = 'estimated_value';
sortOrder = 'desc';
} else if (sort == TendersStrings.minimumTime) {
sortBy = 'submission_deadline';
sortOrder = 'asc';
} else if (sort == TendersStrings.mostOfTheTime) {
sortBy = 'submission_deadline';
sortOrder = 'desc';
}
currentPage = 1;
notifyListeners();
}
void _clearAllData() {
_tendersResponse = null;
_errorMessage = null;
_currentOffset = 0;
_hasMoreData = true;
currentPage = 1;
sortBy = '';
sortOrder = '';
selectedSort = '';
selectedStatus = '';
notifyListeners();
}
void init(BuildContext context) async {
clearAll();
await getTenders(context: context);
}
/// Get feedback for a specific tender /// Get feedback for a specific tender
FeedbackData? getFeedbackForTender(String tenderId) { FeedbackData? getFeedbackForTender(String tenderId) {
try { try {
@@ -61,10 +145,13 @@ class TendersViewModel with ChangeNotifier {
return feedbacks.any((f) => f.tender == tenderId); return feedbacks.any((f) => f.tender == tenderId);
} }
Future<void> getTenders({String? query, BuildContext? context}) async { Future<void> getTenders({
required BuildContext context,
String? query,
}) async {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
if (context != null && DeviceUtils.isDesktop(context)) { if (DeviceUtils.isDesktop(context)) {
await getTendersDesktop(query: query); await getTendersDesktop(query: query);
} else { } else {
await getTendersMobile(query: query); await getTendersMobile(query: query);
@@ -72,7 +159,7 @@ class TendersViewModel with ChangeNotifier {
} }
Future<void> getTendersMobile({String? query}) async { Future<void> getTendersMobile({String? query}) async {
clearAll(); // Clear all data before fetching new tenders // Clear all data before fetching new tenders
_currentOffset = 0; _currentOffset = 0;
_hasMoreData = true; _hasMoreData = true;
@@ -82,6 +169,8 @@ class TendersViewModel with ChangeNotifier {
limit: _pageSize, limit: _pageSize,
offset: _currentOffset, offset: _currentOffset,
query: query, query: query,
sortBy: '',
sortOrder: '',
); );
switch (result) { switch (result) {
case Ok<TendersResponse>(): case Ok<TendersResponse>():
@@ -111,9 +200,16 @@ class TendersViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> getTendersDesktop({String? query}) async { Future<void> getTendersDesktop({
clearAll(); // Clear all data before fetching new tenders String? query,
String? sortBy,
String? sortOrder,
String? publicationDateFrom,
String? publicationDateTo,
}) async {
// Clear all data before fetching new tenders
_isLoading = true;
notifyListeners();
_currentOffset = 0; _currentOffset = 0;
_hasMoreData = true; _hasMoreData = true;
_searchQuery = query; _searchQuery = query;
@@ -122,6 +218,10 @@ class TendersViewModel with ChangeNotifier {
limit: _pageSize, limit: _pageSize,
offset: _currentOffset, offset: _currentOffset,
query: query, query: query,
sortBy: sortBy ?? '',
sortOrder: sortOrder ?? '',
publicationDateFrom: publicationDateFrom ?? '',
publicationDateTo: publicationDateTo ?? '',
); );
switch (result) { switch (result) {
case Ok<TendersResponse>(): case Ok<TendersResponse>():
@@ -166,6 +266,8 @@ class TendersViewModel with ChangeNotifier {
limit: _pageSize, limit: _pageSize,
offset: _currentOffset, offset: _currentOffset,
query: _searchQuery, query: _searchQuery,
sortBy: sortBy,
sortOrder: sortOrder,
); );
switch (result) { switch (result) {
@@ -356,13 +458,24 @@ class TendersViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> callWithSortAndStatus() async {
await getTendersDesktop(
sortBy: sortBy,
sortOrder: sortOrder,
publicationDateFrom: startDateUnix,
publicationDateTo: endDateUnix,
);
}
/// Search tenders with query /// Search tenders with query
Future<void> searchTenders(String query) async { Future<void> searchTenders(String query) async {
if (query.trim().isEmpty) { await getTendersDesktop(
await getTendersDesktop(); query: query.trim(),
} else { sortBy: sortBy,
await getTendersDesktop(query: query.trim()); sortOrder: sortOrder,
} publicationDateFrom: startDateUnix,
publicationDateTo: endDateUnix,
);
} }
/// Clear all data (tenders, feedbacks, and approvals) /// Clear all data (tenders, feedbacks, and approvals)
@@ -370,6 +483,11 @@ class TendersViewModel with ChangeNotifier {
clearTenders(); clearTenders();
clearFeedbacks(); clearFeedbacks();
_searchQuery = null; _searchQuery = null;
selectedSort = '';
selectedStatus = '';
selectedDateRange = null;
sortBy = '';
sortOrder = '';
notifyListeners(); notifyListeners();
} }
@@ -389,6 +507,8 @@ class TendersViewModel with ChangeNotifier {
limit: _pageSize, limit: _pageSize,
offset: _currentOffset, offset: _currentOffset,
query: _searchQuery, query: _searchQuery,
sortBy: sortBy,
sortOrder: sortOrder,
); );
switch (result) { switch (result) {
@@ -68,7 +68,7 @@ class DesktopNavigationWidget extends StatelessWidget {
context, context,
() => const TendersRouteData().go(context), () => const TendersRouteData().go(context),
); );
context.read<TendersViewModel>().getTenders(); context.read<TendersViewModel>().init(context);
} }
}, },
iconPath: AssetsManager.tenders, iconPath: AssetsManager.tenders,
@@ -59,7 +59,7 @@ class TabletNavigationWidget extends StatelessWidget {
context, context,
() => const TendersRouteData().go(context), () => const TendersRouteData().go(context),
); );
context.read<TendersViewModel>().getTenders(); context.read<TendersViewModel>().init(context);
} }
}, },
iconPath: AssetsManager.tenders, iconPath: AssetsManager.tenders,
+1 -1
View File
@@ -21,7 +21,7 @@ class _TendersScreenState extends State<TendersScreen> {
void initState() { void initState() {
viewModel = context.read<TendersViewModel>(); viewModel = context.read<TendersViewModel>();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
viewModel.getTenders(context: context); viewModel.init(context);
}); });
super.initState(); super.initState();
} }
@@ -5,15 +5,16 @@ import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.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/shared/base_button.dart'; import 'package:tm_app/views/shared/base_button.dart';
import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; import 'package:tm_app/views/tenders/strings/tenders_strings.dart';
import '../../../view_models/tenders_view_model.dart';
class TendersFilterDialog extends StatelessWidget { class TendersFilterDialog extends StatelessWidget {
const TendersFilterDialog({super.key}); const TendersFilterDialog({super.key});
Widget _dateRangePill(BuildContext context) { Widget _dateRangePill(BuildContext context) {
final viewModel = context.watch<YourTendersViewModel>(); final viewModel = context.watch<TendersViewModel>();
final start = viewModel.selectedDateRange?.start; final start = viewModel.selectedDateRange?.start;
final end = viewModel.selectedDateRange?.end; final end = viewModel.selectedDateRange?.end;
@@ -97,7 +98,7 @@ class TendersFilterDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<YourTendersViewModel>( return Consumer<TendersViewModel>(
builder: (context, vm, child) { builder: (context, vm, child) {
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
@@ -176,14 +177,14 @@ class TendersFilterDialog extends StatelessWidget {
SizedBox(height: 16.0.h()), SizedBox(height: 16.0.h()),
Row( Row(
children: [ children: [
const Spacer(), const Spacer(),
SizedBox( SizedBox(
width: 156.0.w(), width: 156.0.w(),
child: BaseButton( child: BaseButton(
isEnabled: true, isEnabled: true,
onPressed: () { onPressed: () {
context.pop(); context.pop();
vm.callWithFilter(); vm.callWithSortAndStatus();
}, },
text: TendersStrings.confirm, text: TendersStrings.confirm,
textColor: AppColors.textBlue, textColor: AppColors.textBlue,
@@ -5,16 +5,17 @@ import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.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/shared/base_button.dart'; import 'package:tm_app/views/shared/base_button.dart';
import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; import 'package:tm_app/views/tenders/strings/tenders_strings.dart';
import '../../../view_models/tenders_view_model.dart';
class TendersSortDialog extends StatelessWidget { class TendersSortDialog extends StatelessWidget {
const TendersSortDialog({super.key}); const TendersSortDialog({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<YourTendersViewModel>( return Consumer<TendersViewModel>(
builder: (context, vm, child) { builder: (context, vm, child) {
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
@@ -59,9 +60,9 @@ class TendersSortDialog extends StatelessWidget {
].map((status) { ].map((status) {
return RadioListTile( return RadioListTile(
value: status, value: status,
groupValue: vm.selectedStatus, groupValue: vm.selectedSort,
activeColor: AppColors.mainBlue, activeColor: AppColors.mainBlue,
onChanged: (value) => vm.setStatus(value!), onChanged: (value) => vm.setSort(value!),
title: Text( title: Text(
status, status,
style: TextStyle( style: TextStyle(
@@ -75,14 +76,14 @@ class TendersSortDialog extends StatelessWidget {
SizedBox(height: 16.0.h()), SizedBox(height: 16.0.h()),
Row( Row(
children: [ children: [
const Spacer(), const Spacer(),
SizedBox( SizedBox(
width: 156.0.w(), width: 156.0.w(),
child: BaseButton( child: BaseButton(
isEnabled: true, isEnabled: true,
onPressed: () { onPressed: () {
context.pop(); context.pop();
vm.callWithFilter(); vm.callWithSortAndStatus();
}, },
text: TendersStrings.confirm, text: TendersStrings.confirm,
textColor: AppColors.textBlue, textColor: AppColors.textBlue,