Merge pull request 'search logic added to desktop tenders' (#148) from tenders_search into main

Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/148
This commit is contained in:
a.ghabeli
2025-09-22 09:28:43 +03:30
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({
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<Result<FeedbackResponse>> getTenderFeedback({
+10 -2
View File
@@ -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';
@@ -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<TenderData>? tenders,
}) = _TendersData;
const factory TendersData({required List<TenderData>? tenders}) =
_TendersData;
factory TendersData.fromJson(Map<String, Object?> json) =>
_$TendersDataFromJson(json);
+2 -1
View File
@@ -17,9 +17,10 @@ class TendersService {
Future<Result<TendersResponse>> 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),
);
+100 -35
View File
@@ -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<FeedbackData> feedbacks = [];
List<TenderApprovalsData> tenderApprovals = [];
@@ -95,17 +94,27 @@ class TendersViewModel with ChangeNotifier {
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;
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>():
@@ -138,6 +147,52 @@ class TendersViewModel with ChangeNotifier {
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 {
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<void> 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>():
_tendersResponse = result.value;
break;
case Error<TendersResponse>():
_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>():
_tendersResponse = result.value;
break;
case Error<TendersResponse>():
_errorMessage = result.error.toString();
break;
}
notifyListeners();
}
}
+35 -6
View File
@@ -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<DesktopTendersPage> {
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<DesktopTendersPage> {
}
}
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<TendersViewModel>();
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<int>(
context: context,
builder: (context) =>
PageSelectionDialog(totalPages: totalPages),
builder:
(context) => PageSelectionDialog(totalPages: totalPages),
);
if (selectedPage != null) {
onPageSelected(selectedPage);
+19 -1
View File
@@ -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<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
Widget build(BuildContext context) {
return const ResponsiveBuilder(
@@ -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...';
}