Add filter dialog and bottom sheet for tenders

This commit is contained in:
llsajjad
2025-08-31 16:24:35 +03:30
parent bf6fbea9a9
commit 96540c3cb3
9 changed files with 314 additions and 57 deletions
+1
View File
@@ -51,6 +51,7 @@ class AssetsManager {
static const tickCircle = 'assets/icons/tick-circle.svg';
static const arrowLeft = 'assets/svgs/arrow_left.svg';
static const export = 'assets/svgs/export.svg';
static const trailingIcon = 'assets/icons/trailing_icon.svg';
//profile page
static const sun = 'assets/icons/sun.svg';
+11
View File
@@ -62,6 +62,17 @@ class AppStrings {
//your tenders
static const String yourTendersTitle = 'Your tenders';
static const filter = 'Filter';
static const selectDateRange = 'Select date range';
static const status = 'Status';
static const all = 'All';
static const likeTenders = 'Like tenders';
static const dislikeTenders = 'Dislike tenders';
static const tendersSubmitted = 'Tenders submitted';
static const confirm = 'Confirm';
static const startDate = 'YYYY/MM/DD';
static const endDate = 'YYYY/MM/DD';
// Tender Detail Page
static const String tenderDetailTitle = 'Tender Detail';
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/repositories/your_tenders_repository.dart';
import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart';
@@ -13,6 +14,17 @@ class YourTendersViewModel with ChangeNotifier {
String? errorMessage;
TenderApprovalsResponse? data;
// Filter States
DateTimeRange? selectedDateRange;
String selectedStatus = 'All';
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';
Future<void> getApprovedTenders() async {
isLoading = true;
notifyListeners();
@@ -32,4 +44,15 @@ class YourTendersViewModel with ChangeNotifier {
isLoading = false;
notifyListeners();
}
// --- Filter Actions ---
void setDateRange(DateTimeRange? range) {
selectedDateRange = range;
notifyListeners();
}
void setStatus(String status) {
selectedStatus = status;
notifyListeners();
}
}
@@ -1,11 +1,9 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/view_models/your_tenders_view_model.dart';
import 'package:tm_app/views/shared/main_tab_bar.dart';
import 'package:tm_app/views/your_tenders/widgets/approved_tenders.dart';
import 'package:tm_app/views/your_tenders/widgets/filter_button.dart';
import 'package:tm_app/views/your_tenders/widgets/tablet_desktop_appbar.dart';
import 'package:tm_app/views/your_tenders/widgets/tenders_submitted.dart';
@@ -73,13 +71,16 @@ class _YourTendersDesktopPageState extends State<YourTendersDesktopPage>
SizedBox(height: 64.0),
TabletDesktopAppbar(),
SizedBox(height: 20.0),
MainTabBar(
controller: controller,
titles: [
AppStrings.approvedTenders,
AppStrings.tenderSubmitted,
],
),
// MainTabBar(
// controller: controller,
// titles: [
// AppStrings.approvedTenders,
// AppStrings.tenderSubmitted,
// ],
// ),
FilterButton(platformType: PlatformType.tabletDesktop),
Expanded(
child: Consumer<YourTendersViewModel>(
builder: (context, viewModel, child) {
@@ -98,7 +99,7 @@ class _YourTendersDesktopPageState extends State<YourTendersDesktopPage>
return TabBarView(
controller: controller,
children: [
ApprovedTenders(tendersSubmitted: []),
// ApprovedTenders(tendersSubmitted: []),
TendersSubmitted(approvedTenders: viewModel.data!),
],
);
@@ -3,38 +3,13 @@ import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/view_models/your_tenders_view_model.dart';
import 'package:tm_app/views/shared/main_tab_bar.dart';
import 'package:tm_app/views/shared/tender_app_bar.dart';
import 'package:tm_app/views/your_tenders/widgets/approved_tenders.dart';
import 'package:tm_app/views/your_tenders/widgets/filter_button.dart';
import 'package:tm_app/views/your_tenders/widgets/tenders_submitted.dart';
class YourTendersMobilePage extends StatefulWidget {
class YourTendersMobilePage extends StatelessWidget {
const YourTendersMobilePage({super.key});
@override
State<YourTendersMobilePage> createState() => _YourTendersMobilePageState();
}
class _YourTendersMobilePageState extends State<YourTendersMobilePage>
with SingleTickerProviderStateMixin {
late TabController controller;
@override
void initState() {
super.initState();
controller = TabController(length: 2, vsync: this);
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<YourTendersViewModel>().getApprovedTenders();
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -42,10 +17,7 @@ class _YourTendersMobilePageState extends State<YourTendersMobilePage>
appBar: appBar(context: context, title: AppStrings.yourTenders),
body: Column(
children: [
MainTabBar(
controller: controller,
titles: [AppStrings.approvedTenders, AppStrings.tenderSubmitted],
),
FilterButton(platformType: PlatformType.mobile),
Expanded(
child: Consumer<YourTendersViewModel>(
builder: (context, viewModel, child) {
@@ -61,11 +33,12 @@ class _YourTendersMobilePageState extends State<YourTendersMobilePage>
return Center(child: Text(viewModel.errorMessage!));
}
return TabBarView(
controller: controller,
return Column(
children: [
ApprovedTenders(tendersSubmitted: []),
TendersSubmitted(approvedTenders: viewModel.data!),
// Expanded(child: ApprovedTenders(tendersSubmitted: [])),
Expanded(
child: TendersSubmitted(approvedTenders: viewModel.data!),
),
],
);
},
@@ -2,12 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/strings.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/main_tab_bar.dart';
import 'package:tm_app/views/your_tenders/widgets/approved_tenders.dart';
import 'package:tm_app/views/your_tenders/widgets/filter_button.dart';
import 'package:tm_app/views/your_tenders/widgets/tablet_desktop_appbar.dart';
import 'package:tm_app/views/your_tenders/widgets/tenders_submitted.dart';
@@ -101,13 +99,15 @@ class _YourTendersTabletPageState extends State<YourTendersTabletPage>
child: TabletDesktopAppbar(),
),
SizedBox(height: 24.0.h()),
MainTabBar(
controller: controller,
titles: [
AppStrings.approvedTenders,
AppStrings.tenderSubmitted,
],
),
// MainTabBar(
// controller: controller,
// titles: [
// AppStrings.approvedTenders,
// AppStrings.tenderSubmitted,
// ],
// ),
FilterButton(platformType: PlatformType.tabletDesktop),
Expanded(
child: Consumer<YourTendersViewModel>(
builder: (context, viewModel, child) {
@@ -126,7 +126,7 @@ class _YourTendersTabletPageState extends State<YourTendersTabletPage>
return TabBarView(
controller: controller,
children: [
ApprovedTenders(tendersSubmitted: []),
// ApprovedTenders(tendersSubmitted: []),
TendersSubmitted(approvedTenders: viewModel.data!),
],
);
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'tenders_filter_dialog.dart';
enum PlatformType { mobile, tabletDesktop }
class FilterButton extends StatelessWidget {
final PlatformType platformType;
const FilterButton({required this.platformType, super.key});
void _openFilter(BuildContext context) {
if (platformType == PlatformType.mobile) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (_) => const TendersFilterDialog(),
);
} else {
showDialog(
context: context,
builder:
(_) => Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: const TendersFilterDialog(),
),
);
}
}
@override
Widget build(BuildContext context) {
// Mobile style: container with icon and text
return InkWell(
onTap: () => _openFilter(context),
child: Container(
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.filter_list, color: Colors.blue),
SizedBox(width: 8),
Text(
'Filter',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
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';
class TendersFilterDialog extends StatelessWidget {
const TendersFilterDialog({super.key});
Widget _dateRangePill(BuildContext context) {
final viewModel = context.watch<YourTendersViewModel>();
final start = viewModel.selectedDateRange?.start;
final end = viewModel.selectedDateRange?.end;
Future<void> pick() async {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2020),
lastDate: DateTime(2035),
initialDateRange: viewModel.selectedDateRange,
);
if (picked != null) {
viewModel.setDateRange(picked);
}
}
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: pick,
child: Container(
height: 56.0.h(),
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: AppColors.grey0,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.grey40),
),
child: Center(
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.center,
child: Container(
width: 1,
margin: const EdgeInsets.symmetric(vertical: 8),
color: AppColors.grey40,
),
),
Align(
alignment: Alignment.center,
child: SizedBox(
width: 26.0.w(),
height: 26.0.h(),
child: SvgPicture.asset(AssetsManager.trailingIcon),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
start == null ? 'YYYY/MM/DD' : viewModel.startDateFormatted,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
Text(
end == null ? 'YYYY/MM/DD' : viewModel.endDateFormatted,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
],
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Consumer<YourTendersViewModel>(
builder: (context, vm, child) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
'Filter',
style: TextStyle(
fontSize: 20.0.sp(),
fontWeight: FontWeight.w700,
color: AppColors.grey70,
),
),
),
SizedBox(height: 16.0.h()),
Text(
'Select date range',
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey80,
),
),
SizedBox(height: 20.0.h()),
_dateRangePill(context),
SizedBox(height: 16.0.h()),
Text(
'Status',
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70,
),
),
...[
'All',
'Like tenders',
'Dislike tenders',
'Approved tenders',
'Tenders submitted',
].map((status) {
return RadioListTile(
value: status,
groupValue: vm.selectedStatus,
activeColor: AppColors.mainBlue,
onChanged: (value) => vm.setStatus(value!),
title: Text(
status,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
);
}),
SizedBox(height: 16.0.h()),
SizedBox(
width: double.infinity,
child: BaseButton(
isEnabled: true,
onPressed: () {
context.pop();
},
text: 'Confirm',
textColor: AppColors.mainBlue,
backgroundColor: AppColors.primary30,
),
),
],
),
),
);
},
);
}
}