search logic added to desktop tenders

This commit is contained in:
amirrezaghabeli
2025-09-22 09:28:09 +03:30
parent 2e2718a92e
commit f70229ab30
9 changed files with 200 additions and 51 deletions
+24
View File
@@ -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;
}
}
@@ -14,8 +14,13 @@ class TendersRepository {
Future<Result<TendersResponse>> getTenders({ Future<Result<TendersResponse>> getTenders({
required int limit, required int limit,
required int offset, required int offset,
String? query,
}) async { }) async {
return _tendersService.getTenders(limit: limit, offset: offset); return _tendersService.getTenders(
limit: limit,
offset: offset,
query: query,
);
} }
Future<Result<FeedbackResponse>> getTenderFeedback({ Future<Result<FeedbackResponse>> getTenderFeedback({
+10 -2
View File
@@ -1,7 +1,15 @@
class TendersApi { class TendersApi {
static const String tenders = '/api/v1/tenders'; static const String tenders = '/api/v1/tenders';
static String getTenders({required int limit, required int offset}) { static String getTenders({
return '$tenders?limit=$limit&offset=$offset'; 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'; static const String feedbackTender = '/api/v1/feedback/tender';
@@ -2,7 +2,6 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import '../../meta/meta.dart';
import '../../tender_data/tender_data.dart'; import '../../tender_data/tender_data.dart';
part 'tenders_data.freezed.dart'; part 'tenders_data.freezed.dart';
@@ -10,9 +9,8 @@ part 'tenders_data.g.dart';
@freezed @freezed
abstract class TendersData with _$TendersData { abstract class TendersData with _$TendersData {
const factory TendersData({ const factory TendersData({required List<TenderData>? tenders}) =
required List<TenderData>? tenders, _TendersData;
}) = _TendersData;
factory TendersData.fromJson(Map<String, Object?> json) => factory TendersData.fromJson(Map<String, Object?> json) =>
_$TendersDataFromJson(json); _$TendersDataFromJson(json);
+2 -1
View File
@@ -17,9 +17,10 @@ class TendersService {
Future<Result<TendersResponse>> getTenders({ Future<Result<TendersResponse>> getTenders({
required int limit, required int limit,
required int offset, required int offset,
String? query,
}) async { }) async {
final result = await _networkManager.makeRequest( final result = await _networkManager.makeRequest(
TendersApi.getTenders(limit: limit, offset: offset), TendersApi.getTenders(limit: limit, offset: offset, query: query),
method: 'GET', method: 'GET',
(json) => TendersResponse.fromJson(json), (json) => TendersResponse.fromJson(json),
); );
+73 -8
View File
@@ -1,6 +1,7 @@
import 'dart:developer'; import 'dart:developer';
import 'package:flutter/material.dart'; 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/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';
@@ -22,11 +23,7 @@ class TendersViewModel with ChangeNotifier {
required TendersRepository tendersRepository, required TendersRepository tendersRepository,
required HomeViewModel homeViewModel, required HomeViewModel homeViewModel,
}) : _tendersRepository = tendersRepository, }) : _tendersRepository = tendersRepository,
_homeViewModel = homeViewModel { _homeViewModel = homeViewModel;
WidgetsBinding.instance.addPostFrameCallback((_) {
getTenders();
});
}
bool _isLoading = false; bool _isLoading = false;
bool _isLoadingMore = false; bool _isLoadingMore = false;
@@ -37,6 +34,7 @@ class TendersViewModel with ChangeNotifier {
FeedbackData? _feedbackData; FeedbackData? _feedbackData;
TenderApprovalsData? _tenderApprovalData; TenderApprovalsData? _tenderApprovalData;
TenderApprovalsToggleData? _tenderApprovalToggleData; TenderApprovalsToggleData? _tenderApprovalToggleData;
String? _searchQuery;
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
bool get isLoadingMore => _isLoadingMore; bool get isLoadingMore => _isLoadingMore;
@@ -48,6 +46,7 @@ class TendersViewModel with ChangeNotifier {
TenderApprovalsData? get tenderApprovalData => _tenderApprovalData; TenderApprovalsData? get tenderApprovalData => _tenderApprovalData;
TenderApprovalsToggleData? get tenderApprovalToggleData => TenderApprovalsToggleData? get tenderApprovalToggleData =>
_tenderApprovalToggleData; _tenderApprovalToggleData;
String? get searchQuery => _searchQuery;
List<FeedbackData> feedbacks = []; List<FeedbackData> feedbacks = [];
List<TenderApprovalsData> tenderApprovals = []; List<TenderApprovalsData> tenderApprovals = [];
@@ -95,17 +94,27 @@ class TendersViewModel with ChangeNotifier {
return approval?.status; return approval?.status;
} }
Future<void> getTenders() async { Future<void> getTenders({String? query, BuildContext? context}) async {
if (context != null && DeviceUtils.isDesktop(context)) {
await getTendersDesktop(query: query);
} else {
await getTendersMobile(query: query);
}
}
Future<void> getTendersMobile({String? query}) async {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
clearAll(); // Clear all data before fetching new tenders clearAll(); // Clear all data before fetching new tenders
_currentOffset = 0; _currentOffset = 0;
_hasMoreData = true; _hasMoreData = true;
_searchQuery = query;
final result = await _tendersRepository.getTenders( final result = await _tendersRepository.getTenders(
limit: _pageSize, limit: _pageSize,
offset: _currentOffset, offset: _currentOffset,
query: query,
); );
switch (result) { switch (result) {
case Ok<TendersResponse>(): case Ok<TendersResponse>():
@@ -138,6 +147,52 @@ class TendersViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> 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>():
_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<TendersResponse>():
_errorMessage = result.error.toString();
break;
}
_isLoading = false;
notifyListeners();
_errorMessage = null;
notifyListeners();
}
Future<void> loadMoreTenders() async { Future<void> loadMoreTenders() async {
if (_isLoadingMore || !_hasMoreData) { if (_isLoadingMore || !_hasMoreData) {
return; return;
@@ -151,6 +206,7 @@ class TendersViewModel with ChangeNotifier {
final result = await _tendersRepository.getTenders( final result = await _tendersRepository.getTenders(
limit: _pageSize, limit: _pageSize,
offset: _currentOffset, offset: _currentOffset,
query: _searchQuery,
); );
switch (result) { switch (result) {
@@ -500,6 +556,15 @@ class TendersViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Search tenders with query
Future<void> searchTenders(String query) async {
if (query.trim().isEmpty) {
await getTendersDesktop();
} else {
await getTendersDesktop(query: query.trim());
}
}
/// Clear all data (tenders, feedbacks, and approvals) /// Clear all data (tenders, feedbacks, and approvals)
void clearAll() { void clearAll() {
clearTenders(); clearTenders();
@@ -508,6 +573,7 @@ class TendersViewModel with ChangeNotifier {
_tenderApprovalData = null; _tenderApprovalData = null;
_tenderApprovalToggleData = null; _tenderApprovalToggleData = null;
_tenderApprovalErrorMessage = null; _tenderApprovalErrorMessage = null;
_searchQuery = null;
notifyListeners(); notifyListeners();
} }
@@ -526,6 +592,7 @@ void jumpToPage(int page) async {
final result = await _tendersRepository.getTenders( final result = await _tendersRepository.getTenders(
limit: _pageSize, limit: _pageSize,
offset: _currentOffset, offset: _currentOffset,
query: _searchQuery,
); );
switch (result) { switch (result) {
@@ -539,6 +606,4 @@ void jumpToPage(int page) async {
notifyListeners(); notifyListeners();
} }
} }
+35 -6
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -61,7 +63,9 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
builder: (context, vm, child) { builder: (context, vm, child) {
if (vm.isLoading) { if (vm.isLoading) {
return const Center( return const Center(
child: CircularProgressIndicator(color: AppColors.jellyBean), child: CircularProgressIndicator(
color: AppColors.jellyBean,
),
); );
} }
if (vm.errorMessage != null) { if (vm.errorMessage != null) {
@@ -105,18 +109,43 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
} }
} }
class _SearchBox extends StatelessWidget { class _SearchBox extends StatefulWidget {
const _SearchBox(); 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<TendersViewModel>();
viewModel.searchTenders(query);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: 48.0.h(), height: 48.0.h(),
width: 680, width: 680,
child: TextField( 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( decoration: InputDecoration(
hintText: 'Search', hintText: TendersStrings.search,
hintStyle: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey60), hintStyle: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey60),
prefixIcon: Padding( prefixIcon: Padding(
padding: EdgeInsets.all(12.0.w()), padding: EdgeInsets.all(12.0.w()),
@@ -221,8 +250,8 @@ class _DesktopPagination extends StatelessWidget {
onTap: () async { onTap: () async {
final selectedPage = await showDialog<int>( final selectedPage = await showDialog<int>(
context: context, context: context,
builder: (context) => builder:
PageSelectionDialog(totalPages: totalPages), (context) => PageSelectionDialog(totalPages: totalPages),
); );
if (selectedPage != null) { if (selectedPage != null) {
onPageSelected(selectedPage); onPageSelected(selectedPage);
+19 -1
View File
@@ -1,13 +1,31 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/views/shared/responsive_builder.dart'; import 'package:tm_app/views/shared/responsive_builder.dart';
import '../../../view_models/tenders_view_model.dart';
import 'd_tenders_page.dart'; import 'd_tenders_page.dart';
import 'm_tenders_page.dart'; import 'm_tenders_page.dart';
import 't_tenders_page.dart'; import 't_tenders_page.dart';
class TendersScreen extends StatelessWidget { class TendersScreen extends StatefulWidget {
const TendersScreen({super.key}); const TendersScreen({super.key});
@override
State<TendersScreen> createState() => _TendersScreenState();
}
class _TendersScreenState extends State<TendersScreen> {
late final TendersViewModel viewModel;
@override
void initState() {
viewModel = context.read<TendersViewModel>();
WidgetsBinding.instance.addPostFrameCallback((_) {
viewModel.getTenders(context: context);
});
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const ResponsiveBuilder( return const ResponsiveBuilder(
@@ -12,4 +12,5 @@ class TendersStrings {
static const String notifications = 'Notifications'; static const String notifications = 'Notifications';
static const String of = 'Of'; static const String of = 'Of';
static const String page = 'Page'; static const String page = 'Page';
static const String search = 'Search tenders...';
} }