diff --git a/lib/core/utils/device_utils.dart b/lib/core/utils/device_utils.dart new file mode 100644 index 0000000..4f74618 --- /dev/null +++ b/lib/core/utils/device_utils.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:tm_app/core/utils/breakpoints.dart'; + +/// Utility class for device and platform detection +class DeviceUtils { + /// Check if the current context represents a desktop screen + static bool isDesktop(BuildContext context) { + final size = MediaQuery.sizeOf(context); + return size.displaySize == DisplaySize.medium || + size.displaySize == DisplaySize.large; + } + + /// Check if the current context represents a mobile screen + static bool isMobile(BuildContext context) { + final size = MediaQuery.sizeOf(context); + return size.displaySize == DisplaySize.extraSmall; + } + + /// Check if the current context represents a tablet screen + static bool isTablet(BuildContext context) { + final size = MediaQuery.sizeOf(context); + return size.displaySize == DisplaySize.small; + } +} diff --git a/lib/data/repositories/tenders_repository.dart b/lib/data/repositories/tenders_repository.dart index ed366af..09fcf29 100644 --- a/lib/data/repositories/tenders_repository.dart +++ b/lib/data/repositories/tenders_repository.dart @@ -14,8 +14,13 @@ class TendersRepository { Future> getTenders({ required int limit, required int offset, + String? query, }) async { - return _tendersService.getTenders(limit: limit, offset: offset); + return _tendersService.getTenders( + limit: limit, + offset: offset, + query: query, + ); } Future> getTenderFeedback({ diff --git a/lib/data/services/api/tenders_api.dart b/lib/data/services/api/tenders_api.dart index facce5d..b611861 100644 --- a/lib/data/services/api/tenders_api.dart +++ b/lib/data/services/api/tenders_api.dart @@ -1,7 +1,15 @@ class TendersApi { static const String tenders = '/api/v1/tenders'; - static String getTenders({required int limit, required int offset}) { - return '$tenders?limit=$limit&offset=$offset'; + static String getTenders({ + required int limit, + required int offset, + String? query, + }) { + String url = '$tenders?limit=$limit&offset=$offset'; + if (query != null && query.isNotEmpty) { + url += '&q=${Uri.encodeComponent(query)}'; + } + return url; } static const String feedbackTender = '/api/v1/feedback/tender'; diff --git a/lib/data/services/model/tenders/tenders_data/tenders_data.dart b/lib/data/services/model/tenders/tenders_data/tenders_data.dart index f3f0082..b45dc72 100644 --- a/lib/data/services/model/tenders/tenders_data/tenders_data.dart +++ b/lib/data/services/model/tenders/tenders_data/tenders_data.dart @@ -2,7 +2,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import '../../meta/meta.dart'; import '../../tender_data/tender_data.dart'; part 'tenders_data.freezed.dart'; @@ -10,9 +9,8 @@ part 'tenders_data.g.dart'; @freezed abstract class TendersData with _$TendersData { - const factory TendersData({ - required List? tenders, - }) = _TendersData; + const factory TendersData({required List? tenders}) = + _TendersData; factory TendersData.fromJson(Map json) => _$TendersDataFromJson(json); diff --git a/lib/data/services/tenders_service.dart b/lib/data/services/tenders_service.dart index 216045f..11ea50e 100644 --- a/lib/data/services/tenders_service.dart +++ b/lib/data/services/tenders_service.dart @@ -17,9 +17,10 @@ class TendersService { Future> getTenders({ required int limit, required int offset, + String? query, }) async { final result = await _networkManager.makeRequest( - TendersApi.getTenders(limit: limit, offset: offset), + TendersApi.getTenders(limit: limit, offset: offset, query: query), 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 9f4fc59..e4d9de8 100644 --- a/lib/view_models/tenders_view_model.dart +++ b/lib/view_models/tenders_view_model.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:flutter/material.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'; @@ -22,11 +23,7 @@ class TendersViewModel with ChangeNotifier { required TendersRepository tendersRepository, required HomeViewModel homeViewModel, }) : _tendersRepository = tendersRepository, - _homeViewModel = homeViewModel { - WidgetsBinding.instance.addPostFrameCallback((_) { - getTenders(); - }); - } + _homeViewModel = homeViewModel; bool _isLoading = false; bool _isLoadingMore = false; @@ -37,6 +34,7 @@ class TendersViewModel with ChangeNotifier { FeedbackData? _feedbackData; TenderApprovalsData? _tenderApprovalData; TenderApprovalsToggleData? _tenderApprovalToggleData; + String? _searchQuery; bool get isLoading => _isLoading; bool get isLoadingMore => _isLoadingMore; @@ -48,6 +46,7 @@ class TendersViewModel with ChangeNotifier { TenderApprovalsData? get tenderApprovalData => _tenderApprovalData; TenderApprovalsToggleData? get tenderApprovalToggleData => _tenderApprovalToggleData; + String? get searchQuery => _searchQuery; List feedbacks = []; List tenderApprovals = []; @@ -95,17 +94,27 @@ class TendersViewModel with ChangeNotifier { return approval?.status; } - Future getTenders() async { + Future getTenders({String? query, BuildContext? context}) async { + if (context != null && DeviceUtils.isDesktop(context)) { + await getTendersDesktop(query: query); + } else { + await getTendersMobile(query: query); + } + } + + Future getTendersMobile({String? query}) async { _isLoading = true; notifyListeners(); clearAll(); // Clear all data before fetching new tenders _currentOffset = 0; _hasMoreData = true; + _searchQuery = query; final result = await _tendersRepository.getTenders( limit: _pageSize, offset: _currentOffset, + query: query, ); switch (result) { case Ok(): @@ -138,6 +147,52 @@ class TendersViewModel with ChangeNotifier { notifyListeners(); } + Future getTendersDesktop({String? query}) async { + _isLoading = true; + notifyListeners(); + clearAll(); // Clear all data before fetching new tenders + + _currentOffset = 0; + _hasMoreData = true; + _searchQuery = query; + + final result = await _tendersRepository.getTenders( + limit: _pageSize, + offset: _currentOffset, + query: query, + ); + switch (result) { + case Ok(): + _tendersResponse = result.value; + // Check if we have more data + if (_tendersResponse?.data?.tenders != null) { + _hasMoreData = _tendersResponse!.data!.tenders!.length >= _pageSize; + } + if (_tendersResponse?.data?.tenders?.isNotEmpty == true) { + // Fetch feedback and approvals for the first tender only + // getTenderFeedback(_tendersResponse!.data!.tenders!.first.id!); + // getTenderApprovals(_tendersResponse!.data!.tenders!.first.id!); + for (var tender in _tendersResponse!.data!.tenders!) { + getTenderFeedback(tender.id!); + } + } + if (_tendersResponse?.meta?.pages != null) { + _totalPages = _tendersResponse!.meta!.pages!; + } else { + _totalPages = 1; + } + notifyListeners(); + break; + case Error(): + _errorMessage = result.error.toString(); + break; + } + _isLoading = false; + notifyListeners(); + _errorMessage = null; + notifyListeners(); + } + Future loadMoreTenders() async { if (_isLoadingMore || !_hasMoreData) { return; @@ -151,6 +206,7 @@ class TendersViewModel with ChangeNotifier { final result = await _tendersRepository.getTenders( limit: _pageSize, offset: _currentOffset, + query: _searchQuery, ); switch (result) { @@ -500,6 +556,15 @@ class TendersViewModel with ChangeNotifier { notifyListeners(); } + /// Search tenders with query + Future searchTenders(String query) async { + if (query.trim().isEmpty) { + await getTendersDesktop(); + } else { + await getTendersDesktop(query: query.trim()); + } + } + /// Clear all data (tenders, feedbacks, and approvals) void clearAll() { clearTenders(); @@ -508,37 +573,37 @@ class TendersViewModel with ChangeNotifier { _tenderApprovalData = null; _tenderApprovalToggleData = null; _tenderApprovalErrorMessage = null; + _searchQuery = null; notifyListeners(); } -void jumpToPage(int page) async { - final meta = tendersResponse?.meta; - if (meta?.pages == null) { - return; + void jumpToPage(int page) async { + final meta = tendersResponse?.meta; + if (meta?.pages == null) { + return; + } + if (page < 1 || page > meta!.pages!) { + return; + } + + currentPage = page; + _currentOffset = (page - 1) * _pageSize; + + final result = await _tendersRepository.getTenders( + limit: _pageSize, + offset: _currentOffset, + query: _searchQuery, + ); + + switch (result) { + case Ok(): + _tendersResponse = result.value; + break; + case Error(): + _errorMessage = result.error.toString(); + break; + } + + notifyListeners(); } - if (page < 1 || page > meta!.pages!) { - return; - } - - currentPage = page; - _currentOffset = (page - 1) * _pageSize; - - final result = await _tendersRepository.getTenders( - limit: _pageSize, - offset: _currentOffset, - ); - - switch (result) { - case Ok(): - _tendersResponse = result.value; - break; - case Error(): - _errorMessage = result.error.toString(); - break; - } - - notifyListeners(); -} - - } diff --git a/lib/views/tenders/pages/d_tenders_page.dart b/lib/views/tenders/pages/d_tenders_page.dart index 0dff86c..e5bced0 100644 --- a/lib/views/tenders/pages/d_tenders_page.dart +++ b/lib/views/tenders/pages/d_tenders_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; @@ -61,7 +63,9 @@ class _DesktopTendersPageState extends State { builder: (context, vm, child) { if (vm.isLoading) { return const Center( - child: CircularProgressIndicator(color: AppColors.jellyBean), + child: CircularProgressIndicator( + color: AppColors.jellyBean, + ), ); } if (vm.errorMessage != null) { @@ -105,18 +109,43 @@ class _DesktopTendersPageState extends State { } } -class _SearchBox extends StatelessWidget { +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( - style: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey0), + controller: _searchController, + onChanged: _onSearchChanged, + style: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey60), decoration: InputDecoration( - hintText: 'Search', + hintText: TendersStrings.search, hintStyle: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey60), prefixIcon: Padding( padding: EdgeInsets.all(12.0.w()), @@ -221,8 +250,8 @@ class _DesktopPagination extends StatelessWidget { onTap: () async { final selectedPage = await showDialog( context: context, - builder: (context) => - PageSelectionDialog(totalPages: totalPages), + builder: + (context) => PageSelectionDialog(totalPages: totalPages), ); if (selectedPage != null) { onPageSelected(selectedPage); diff --git a/lib/views/tenders/pages/tenders_screen.dart b/lib/views/tenders/pages/tenders_screen.dart index d84ad64..824f2e7 100644 --- a/lib/views/tenders/pages/tenders_screen.dart +++ b/lib/views/tenders/pages/tenders_screen.dart @@ -1,13 +1,31 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:tm_app/views/shared/responsive_builder.dart'; +import '../../../view_models/tenders_view_model.dart'; import 'd_tenders_page.dart'; import 'm_tenders_page.dart'; import 't_tenders_page.dart'; -class TendersScreen extends StatelessWidget { +class TendersScreen extends StatefulWidget { const TendersScreen({super.key}); + @override + State createState() => _TendersScreenState(); +} + +class _TendersScreenState extends State { + late final TendersViewModel viewModel; + + @override + void initState() { + viewModel = context.read(); + WidgetsBinding.instance.addPostFrameCallback((_) { + viewModel.getTenders(context: context); + }); + super.initState(); + } + @override Widget build(BuildContext context) { return const ResponsiveBuilder( diff --git a/lib/views/tenders/strings/tenders_strings.dart b/lib/views/tenders/strings/tenders_strings.dart index 259d4a8..f0c5034 100644 --- a/lib/views/tenders/strings/tenders_strings.dart +++ b/lib/views/tenders/strings/tenders_strings.dart @@ -10,6 +10,7 @@ class TendersStrings { static const String tenderDeadlineLabel = 'Deadline'; static const String contracts = 'Contracts'; static const String notifications = 'Notifications'; - static const String of= 'Of'; + static const String of = 'Of'; static const String page = 'Page'; + static const String search = 'Search tenders...'; }