diff --git a/lib/core/routes/app_shell/app_shell_screen.dart b/lib/core/routes/app_shell/app_shell_screen.dart index a45123e..fd8c155 100644 --- a/lib/core/routes/app_shell/app_shell_screen.dart +++ b/lib/core/routes/app_shell/app_shell_screen.dart @@ -30,7 +30,7 @@ class AppShellScreen extends StatelessWidget { context.read().init(); } if (index == 1) { - context.read().getTenders(); + context.read().init(context); } }); } diff --git a/lib/data/repositories/tenders_repository.dart b/lib/data/repositories/tenders_repository.dart index 09fcf29..567e010 100644 --- a/lib/data/repositories/tenders_repository.dart +++ b/lib/data/repositories/tenders_repository.dart @@ -15,11 +15,19 @@ class TendersRepository { required int limit, required int offset, String? query, + String? sortBy, + String? sortOrder, + String? publicationDateFrom, + String? publicationDateTo, }) async { return _tendersService.getTenders( limit: limit, offset: offset, query: query, + sortBy: sortBy, + sortOrder: sortOrder, + publicationDateFrom: publicationDateFrom, + publicationDateTo: publicationDateTo, ); } diff --git a/lib/data/services/api/tenders_api.dart b/lib/data/services/api/tenders_api.dart index b611861..1b582e4 100644 --- a/lib/data/services/api/tenders_api.dart +++ b/lib/data/services/api/tenders_api.dart @@ -1,14 +1,33 @@ +import 'package:tm_app/core/utils/logger.dart'; + class TendersApi { static const String tenders = '/api/v1/tenders'; static String getTenders({ required int limit, required int offset, String? query, + String? sortBy, + String? sortOrder, + String? publicationDateFrom, + String? publicationDateTo, }) { String url = '$tenders?limit=$limit&offset=$offset'; if (query != null && query.isNotEmpty) { 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; } diff --git a/lib/data/services/tenders_service.dart b/lib/data/services/tenders_service.dart index 11ea50e..1c1c668 100644 --- a/lib/data/services/tenders_service.dart +++ b/lib/data/services/tenders_service.dart @@ -18,9 +18,22 @@ class TendersService { required int limit, required int offset, String? query, + String? sortBy, + String? sortOrder, + String? publicationDateFrom, + String? publicationDateTo, }) async { 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', (json) => TendersResponse.fromJson(json), ); diff --git a/lib/view_models/tenders_view_model.dart b/lib/view_models/tenders_view_model.dart index ac36abd..6661231 100644 --- a/lib/view_models/tenders_view_model.dart +++ b/lib/view_models/tenders_view_model.dart @@ -1,11 +1,13 @@ import 'dart:developer'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:tm_app/core/utils/device_utils.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/tenders/tenders_response/tenders_response.dart'; +import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; import '../core/utils/result.dart'; import 'home_view_model.dart'; @@ -47,6 +49,88 @@ class TendersViewModel with ChangeNotifier { 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 FeedbackData? getFeedbackForTender(String tenderId) { try { @@ -61,10 +145,13 @@ class TendersViewModel with ChangeNotifier { return feedbacks.any((f) => f.tender == tenderId); } - Future getTenders({String? query, BuildContext? context}) async { + Future getTenders({ + required BuildContext context, + String? query, + }) async { _isLoading = true; notifyListeners(); - if (context != null && DeviceUtils.isDesktop(context)) { + if (DeviceUtils.isDesktop(context)) { await getTendersDesktop(query: query); } else { await getTendersMobile(query: query); @@ -72,7 +159,7 @@ class TendersViewModel with ChangeNotifier { } Future getTendersMobile({String? query}) async { - clearAll(); // Clear all data before fetching new tenders + // Clear all data before fetching new tenders _currentOffset = 0; _hasMoreData = true; @@ -82,6 +169,8 @@ class TendersViewModel with ChangeNotifier { limit: _pageSize, offset: _currentOffset, query: query, + sortBy: '', + sortOrder: '', ); switch (result) { case Ok(): @@ -111,9 +200,16 @@ class TendersViewModel with ChangeNotifier { notifyListeners(); } - Future getTendersDesktop({String? query}) async { - clearAll(); // Clear all data before fetching new tenders - + Future getTendersDesktop({ + String? query, + String? sortBy, + String? sortOrder, + String? publicationDateFrom, + String? publicationDateTo, + }) async { + // Clear all data before fetching new tenders + _isLoading = true; + notifyListeners(); _currentOffset = 0; _hasMoreData = true; _searchQuery = query; @@ -122,6 +218,10 @@ class TendersViewModel with ChangeNotifier { limit: _pageSize, offset: _currentOffset, query: query, + sortBy: sortBy ?? '', + sortOrder: sortOrder ?? '', + publicationDateFrom: publicationDateFrom ?? '', + publicationDateTo: publicationDateTo ?? '', ); switch (result) { case Ok(): @@ -166,6 +266,8 @@ class TendersViewModel with ChangeNotifier { limit: _pageSize, offset: _currentOffset, query: _searchQuery, + sortBy: sortBy, + sortOrder: sortOrder, ); switch (result) { @@ -356,13 +458,24 @@ class TendersViewModel with ChangeNotifier { notifyListeners(); } + Future callWithSortAndStatus() async { + await getTendersDesktop( + sortBy: sortBy, + sortOrder: sortOrder, + publicationDateFrom: startDateUnix, + publicationDateTo: endDateUnix, + ); + } + /// Search tenders with query Future searchTenders(String query) async { - if (query.trim().isEmpty) { - await getTendersDesktop(); - } else { - await getTendersDesktop(query: query.trim()); - } + await getTendersDesktop( + query: query.trim(), + sortBy: sortBy, + sortOrder: sortOrder, + publicationDateFrom: startDateUnix, + publicationDateTo: endDateUnix, + ); } /// Clear all data (tenders, feedbacks, and approvals) @@ -370,6 +483,11 @@ class TendersViewModel with ChangeNotifier { clearTenders(); clearFeedbacks(); _searchQuery = null; + selectedSort = ''; + selectedStatus = ''; + selectedDateRange = null; + sortBy = ''; + sortOrder = ''; notifyListeners(); } @@ -389,6 +507,8 @@ class TendersViewModel with ChangeNotifier { limit: _pageSize, offset: _currentOffset, query: _searchQuery, + sortBy: sortBy, + sortOrder: sortOrder, ); switch (result) { diff --git a/lib/views/shared/desktop_navigation_widget.dart b/lib/views/shared/desktop_navigation_widget.dart index 93d3171..7447f7e 100644 --- a/lib/views/shared/desktop_navigation_widget.dart +++ b/lib/views/shared/desktop_navigation_widget.dart @@ -68,7 +68,7 @@ class DesktopNavigationWidget extends StatelessWidget { context, () => const TendersRouteData().go(context), ); - context.read().getTenders(); + context.read().init(context); } }, iconPath: AssetsManager.tenders, diff --git a/lib/views/shared/tablet_navigation_widget.dart b/lib/views/shared/tablet_navigation_widget.dart index 4e1c330..0082cfb 100644 --- a/lib/views/shared/tablet_navigation_widget.dart +++ b/lib/views/shared/tablet_navigation_widget.dart @@ -59,7 +59,7 @@ class TabletNavigationWidget extends StatelessWidget { context, () => const TendersRouteData().go(context), ); - context.read().getTenders(); + context.read().init(context); } }, iconPath: AssetsManager.tenders, diff --git a/lib/views/tenders/pages/tenders_screen.dart b/lib/views/tenders/pages/tenders_screen.dart index 824f2e7..6c3940a 100644 --- a/lib/views/tenders/pages/tenders_screen.dart +++ b/lib/views/tenders/pages/tenders_screen.dart @@ -21,7 +21,7 @@ class _TendersScreenState extends State { void initState() { viewModel = context.read(); WidgetsBinding.instance.addPostFrameCallback((_) { - viewModel.getTenders(context: context); + viewModel.init(context); }); super.initState(); } diff --git a/lib/views/tenders/widgets/tenders_filter_dialog.dart b/lib/views/tenders/widgets/tenders_filter_dialog.dart index 33a623d..144b5ac 100644 --- a/lib/views/tenders/widgets/tenders_filter_dialog.dart +++ b/lib/views/tenders/widgets/tenders_filter_dialog.dart @@ -5,15 +5,16 @@ import 'package:provider/provider.dart'; import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/size_config.dart'; -import 'package:tm_app/view_models/your_tenders_view_model.dart'; import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; +import '../../../view_models/tenders_view_model.dart'; + class TendersFilterDialog extends StatelessWidget { const TendersFilterDialog({super.key}); Widget _dateRangePill(BuildContext context) { - final viewModel = context.watch(); + final viewModel = context.watch(); final start = viewModel.selectedDateRange?.start; final end = viewModel.selectedDateRange?.end; @@ -97,7 +98,7 @@ class TendersFilterDialog extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( + return Consumer( builder: (context, vm, child) { return SingleChildScrollView( child: Padding( @@ -176,14 +177,14 @@ class TendersFilterDialog extends StatelessWidget { SizedBox(height: 16.0.h()), Row( children: [ - const Spacer(), + const Spacer(), SizedBox( width: 156.0.w(), child: BaseButton( isEnabled: true, onPressed: () { context.pop(); - vm.callWithFilter(); + vm.callWithSortAndStatus(); }, text: TendersStrings.confirm, textColor: AppColors.textBlue, diff --git a/lib/views/tenders/widgets/tenders_sort_dialog.dart b/lib/views/tenders/widgets/tenders_sort_dialog.dart index 7cf9a51..2dc757b 100644 --- a/lib/views/tenders/widgets/tenders_sort_dialog.dart +++ b/lib/views/tenders/widgets/tenders_sort_dialog.dart @@ -5,16 +5,17 @@ import 'package:provider/provider.dart'; import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/size_config.dart'; -import 'package:tm_app/view_models/your_tenders_view_model.dart'; import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; +import '../../../view_models/tenders_view_model.dart'; + class TendersSortDialog extends StatelessWidget { const TendersSortDialog({super.key}); - + @override Widget build(BuildContext context) { - return Consumer( + return Consumer( builder: (context, vm, child) { return SingleChildScrollView( child: Padding( @@ -59,9 +60,9 @@ class TendersSortDialog extends StatelessWidget { ].map((status) { return RadioListTile( value: status, - groupValue: vm.selectedStatus, + groupValue: vm.selectedSort, activeColor: AppColors.mainBlue, - onChanged: (value) => vm.setStatus(value!), + onChanged: (value) => vm.setSort(value!), title: Text( status, style: TextStyle( @@ -75,14 +76,14 @@ class TendersSortDialog extends StatelessWidget { SizedBox(height: 16.0.h()), Row( children: [ - const Spacer(), + const Spacer(), SizedBox( width: 156.0.w(), child: BaseButton( isEnabled: true, onPressed: () { context.pop(); - vm.callWithFilter(); + vm.callWithSortAndStatus(); }, text: TendersStrings.confirm, textColor: AppColors.textBlue,