Refactor tender dialogs to utilize ViewModel and improve state management

- Updated `TendersFilterDialog` and `TendersSortDialog` to use a stateful approach, allowing for better management of selected filters and sorting options.
- Integrated `TendersViewModel` into both dialogs, enabling direct interaction with the view model for setting date ranges and statuses.
- Enhanced the UI to reflect changes in state, ensuring a more responsive user experience when applying filters and sorting.
- Updated the `d_tenders_page.dart` to pass the view model and handle filter and sort actions appropriately.
This commit is contained in:
amirrezaghabeli
2025-10-12 15:29:54 +03:30
parent 6d674a4a1b
commit 79a0ecd87f
3 changed files with 322 additions and 252 deletions
+18 -4
View File
@@ -185,6 +185,7 @@ class _ActionButtons extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final viewModel = context.read<TendersViewModel>();
return Container( return Container(
margin: EdgeInsets.only(top: 25.0.h()), margin: EdgeInsets.only(top: 25.0.h()),
height: 40.0.h(), height: 40.0.h(),
@@ -203,9 +204,16 @@ class _ActionButtons extends StatelessWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: const SizedBox( child: SizedBox(
width: 560, width: 560,
child: TendersFilterDialog(), child: TendersFilterDialog(
viewModel: viewModel,
onFilterChanged: (dateRange, status) {
viewModel.setDateRange(dateRange);
viewModel.setStatus(status!);
viewModel.callWithSortAndStatus();
},
),
), ),
), ),
); );
@@ -229,9 +237,15 @@ class _ActionButtons extends StatelessWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: const SizedBox( child: SizedBox(
width: 560, width: 560,
child: TendersSortDialog(), child: TendersSortDialog(
viewModel: viewModel,
onConfirm: (sort) {
viewModel.setSort(sort);
viewModel.callWithSortAndStatus();
},
),
), ),
), ),
); );
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:intl/intl.dart';
import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
@@ -10,194 +10,234 @@ import 'package:tm_app/views/tenders/strings/tenders_strings.dart';
import '../../../view_models/tenders_view_model.dart'; import '../../../view_models/tenders_view_model.dart';
class TendersFilterDialog extends StatelessWidget { class TendersFilterDialog extends StatefulWidget {
const TendersFilterDialog({super.key}); final TendersViewModel viewModel;
final Function(DateTimeRange? dateRange, String? status) onFilterChanged;
const TendersFilterDialog({
required this.viewModel,
required this.onFilterChanged,
super.key,
});
Widget _dateRangePill(BuildContext context) { @override
final viewModel = context.watch<TendersViewModel>(); State<TendersFilterDialog> createState() => _TendersFilterDialogState();
final start = viewModel.selectedDateRange?.start; }
final end = viewModel.selectedDateRange?.end;
Future<void> pick() async { class _TendersFilterDialogState extends State<TendersFilterDialog> {
final picked = await showDateRangePicker( DateTimeRange? dateRange;
context: context, String? status = TendersStrings.all;
firstDate: DateTime(2020),
lastDate: DateTime(2035),
initialDateRange: viewModel.selectedDateRange,
);
if (picked != null) {
viewModel.setDateRange(picked);
}
}
return SizedBox( final _fmt = DateFormat('yyyy/MM/dd');
width: 560,
child: InkWell( String get startDateFormatted =>
borderRadius: BorderRadius.circular(12), dateRange?.start != null ? _fmt.format(dateRange!.start) : 'YYYY/MM/DD';
onTap: pick,
child: Container( String get endDateFormatted =>
height: 56.0.h(), dateRange?.end != null ? _fmt.format(dateRange!.end) : 'YYYY/MM/DD';
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration( String get startDateUnix =>
color: AppColors.grey0, dateRange?.start != null
borderRadius: BorderRadius.circular(12), ? (dateRange!.start.millisecondsSinceEpoch ~/ 1000).toString()
border: Border.all(color: AppColors.grey40), : '';
),
child: Center( String get endDateUnix =>
child: Stack( dateRange?.end != null
alignment: Alignment.center, ? (dateRange!.end.millisecondsSinceEpoch ~/ 1000).toString()
children: [ : '';
Align(
alignment: Alignment.center, @override
child: Container( void initState() {
width: 1, super.initState();
margin: const EdgeInsets.symmetric(vertical: 8), dateRange = widget.viewModel.selectedDateRange;
color: AppColors.grey40, status = _mapApiValueToDisplayString(widget.viewModel.selectedStatus);
), }
),
Align( String _mapApiValueToDisplayString(String apiValue) {
alignment: Alignment.center, final mapping = {
child: SizedBox( 'All': TendersStrings.all,
width: 26.0.w(), 'Read': TendersStrings.read,
height: 26.0.h(), 'Unread': TendersStrings.unread,
child: SvgPicture.asset(AssetsManager.trailingIcon), };
), return mapping[apiValue] ?? TendersStrings.all;
), }
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, Future<void> pick() async {
children: [ final picked = await showDateRangePicker(
Text( context: context,
start == null firstDate: DateTime(2000),
? TendersStrings.startDate lastDate: DateTime(2050),
: viewModel.startDateFormatted, initialDateRange: dateRange,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
Text(
end == null
? TendersStrings.endDate
: viewModel.endDateFormatted,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
],
),
],
),
),
),
),
); );
if (picked != null) {
dateRange = picked;
setState(() {});
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<TendersViewModel>( return SingleChildScrollView(
builder: (context, vm, child) { child: Padding(
return SingleChildScrollView( padding: const EdgeInsets.all(16),
child: Padding( child: Column(
padding: const EdgeInsets.all(16), mainAxisSize: MainAxisSize.min,
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, children: [
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ children: [
Row(
children: [
Text(
TendersStrings.filter,
style: TextStyle(
fontSize: 20.0.sp(),
fontWeight: FontWeight.w700,
color: AppColors.grey70,
),
),
const Spacer(),
InkWell(
onTap: () => Navigator.pop(context),
child: SvgPicture.asset(
AssetsManager.closeCircle,
colorFilter: ColorFilter.mode(
AppColors.grey80,
BlendMode.srcIn,
),
width: 24.0.w(),
height: 24.0.h(),
),
),
],
),
SizedBox(height: 16.0.h()),
Divider(color: AppColors.grey20),
SizedBox(height: 20.0.h()),
Text( Text(
TendersStrings.selectDataRange, TendersStrings.filter,
style: TextStyle( style: TextStyle(
fontSize: 16.0.sp(), fontSize: 20.0.sp(),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w700,
color: AppColors.grey80,
),
),
SizedBox(height: 10.0.h()),
_dateRangePill(context),
SizedBox(height: 16.0.h()),
Text(
TendersStrings.status,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70, color: AppColors.grey70,
), ),
), ),
...[ const Spacer(),
TendersStrings.all, InkWell(
TendersStrings.read, onTap: () => Navigator.pop(context),
TendersStrings.unread, child: SvgPicture.asset(
].map((status) { AssetsManager.closeCircle,
return RadioListTile( colorFilter: ColorFilter.mode(
value: status, AppColors.grey80,
groupValue: vm.selectedStatus, BlendMode.srcIn,
activeColor: AppColors.mainBlue,
onChanged: (value) => vm.setStatus(value!),
title: Text(
status,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
), ),
); width: 24.0.w(),
}), height: 24.0.h(),
SizedBox(height: 16.0.h()), ),
Row(
children: [
const Spacer(),
SizedBox(
width: 156.0.w(),
child: BaseButton(
isEnabled: true,
onPressed: () {
context.pop();
vm.callWithSortAndStatus();
},
text: TendersStrings.confirm,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary2,
),
),
],
), ),
], ],
), ),
), SizedBox(height: 16.0.h()),
); Divider(color: AppColors.grey20),
}, SizedBox(height: 20.0.h()),
Text(
TendersStrings.selectDataRange,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey80,
),
),
SizedBox(height: 10.0.h()),
SizedBox(
width: 560,
child: 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(
dateRange?.start == null
? TendersStrings.startDate
: startDateFormatted,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
Text(
dateRange?.end == null
? TendersStrings.endDate
: endDateFormatted,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
],
),
],
),
),
),
),
),
SizedBox(height: 16.0.h()),
Text(
TendersStrings.status,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70,
),
),
...[
TendersStrings.all,
TendersStrings.read,
TendersStrings.unread,
].map((status) {
return RadioListTile(
value: status,
groupValue: this.status,
activeColor: AppColors.mainBlue,
onChanged:
(value) => setState(() {
this.status = value!;
}),
title: Text(
status,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
);
}),
SizedBox(height: 16.0.h()),
Row(
children: [
const Spacer(),
SizedBox(
width: 156.0.w(),
child: BaseButton(
isEnabled: true,
onPressed: () {
context.pop();
widget.onFilterChanged(dateRange, status);
},
text: TendersStrings.confirm,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary2,
),
),
],
),
],
),
),
); );
} }
} }
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.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/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
@@ -10,91 +9,108 @@ import 'package:tm_app/views/tenders/strings/tenders_strings.dart';
import '../../../view_models/tenders_view_model.dart'; import '../../../view_models/tenders_view_model.dart';
class TendersSortDialog extends StatelessWidget { class TendersSortDialog extends StatefulWidget {
const TendersSortDialog({super.key}); const TendersSortDialog({
required this.viewModel,
required this.onConfirm,
super.key,
});
final TendersViewModel viewModel;
final ValueChanged<String> onConfirm;
@override
State<TendersSortDialog> createState() => _TendersSortDialogState();
}
class _TendersSortDialogState extends State<TendersSortDialog> {
String sort = '';
@override
void initState() {
super.initState();
sort = widget.viewModel.selectedSort;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<TendersViewModel>( return SingleChildScrollView(
builder: (context, vm, child) { child: Padding(
return SingleChildScrollView( padding: const EdgeInsets.all(16),
child: Padding( child: Column(
padding: const EdgeInsets.all(16), mainAxisSize: MainAxisSize.min,
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, children: [
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ children: [
Row( Text(
children: [ TendersStrings.sort,
Text( style: TextStyle(
TendersStrings.sort, fontSize: 20.0.sp(),
style: TextStyle( fontWeight: FontWeight.w700,
fontSize: 20.0.sp(), color: AppColors.grey70,
fontWeight: FontWeight.w700, ),
color: AppColors.grey70,
),
),
const Spacer(),
InkWell(
onTap: () => Navigator.pop(context),
child: SvgPicture.asset(
AssetsManager.closeCircle,
colorFilter: ColorFilter.mode(
AppColors.grey80,
BlendMode.srcIn,
),
width: 24.0.w(),
height: 24.0.h(),
),
),
],
), ),
SizedBox(height: 16.0.h()), const Spacer(),
Divider(color: AppColors.grey20), InkWell(
SizedBox(height: 20.0.h()), onTap: () => Navigator.pop(context),
...[ child: SvgPicture.asset(
TendersStrings.minimumTime, AssetsManager.closeCircle,
TendersStrings.mostOfTheTime, colorFilter: ColorFilter.mode(
].map((status) { AppColors.grey80,
return RadioListTile( BlendMode.srcIn,
value: status,
groupValue: vm.selectedSort,
activeColor: AppColors.mainBlue,
onChanged: (value) => vm.setSort(value!),
title: Text(
status,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
), ),
); width: 24.0.w(),
}), height: 24.0.h(),
SizedBox(height: 16.0.h()), ),
Row(
children: [
const Spacer(),
SizedBox(
width: 156.0.w(),
child: BaseButton(
isEnabled: true,
onPressed: () {
context.pop();
vm.callWithSortAndStatus();
},
text: TendersStrings.confirm,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary2,
),
),
],
), ),
], ],
), ),
), SizedBox(height: 16.0.h()),
); Divider(color: AppColors.grey20),
}, SizedBox(height: 20.0.h()),
...[TendersStrings.minimumTime, TendersStrings.mostOfTheTime].map((
status,
) {
return RadioListTile(
value: status,
groupValue: sort,
activeColor: AppColors.mainBlue,
onChanged:
(value) => setState(() {
sort = value!;
}),
title: Text(
status,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
);
}),
SizedBox(height: 16.0.h()),
Row(
children: [
const Spacer(),
SizedBox(
width: 156.0.w(),
child: BaseButton(
isEnabled: true,
onPressed: () {
context.pop();
widget.onConfirm(sort);
},
text: TendersStrings.confirm,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary2,
),
),
],
),
],
),
),
); );
} }
} }