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:
@@ -185,6 +185,7 @@ class _ActionButtons extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = context.read<TendersViewModel>();
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 25.0.h()),
|
||||
height: 40.0.h(),
|
||||
@@ -203,9 +204,16 @@ class _ActionButtons extends StatelessWidget {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const SizedBox(
|
||||
child: SizedBox(
|
||||
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(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const SizedBox(
|
||||
child: SizedBox(
|
||||
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_svg/svg.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/theme/colors.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';
|
||||
|
||||
class TendersFilterDialog extends StatelessWidget {
|
||||
const TendersFilterDialog({super.key});
|
||||
class TendersFilterDialog extends StatefulWidget {
|
||||
final TendersViewModel viewModel;
|
||||
final Function(DateTimeRange? dateRange, String? status) onFilterChanged;
|
||||
const TendersFilterDialog({
|
||||
required this.viewModel,
|
||||
required this.onFilterChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
Widget _dateRangePill(BuildContext context) {
|
||||
final viewModel = context.watch<TendersViewModel>();
|
||||
final start = viewModel.selectedDateRange?.start;
|
||||
final end = viewModel.selectedDateRange?.end;
|
||||
@override
|
||||
State<TendersFilterDialog> createState() => _TendersFilterDialogState();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
class _TendersFilterDialogState extends State<TendersFilterDialog> {
|
||||
DateTimeRange? dateRange;
|
||||
String? status = TendersStrings.all;
|
||||
|
||||
return 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(
|
||||
start == null
|
||||
? TendersStrings.startDate
|
||||
: viewModel.startDateFormatted,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
final _fmt = DateFormat('yyyy/MM/dd');
|
||||
|
||||
String get startDateFormatted =>
|
||||
dateRange?.start != null ? _fmt.format(dateRange!.start) : 'YYYY/MM/DD';
|
||||
|
||||
String get endDateFormatted =>
|
||||
dateRange?.end != null ? _fmt.format(dateRange!.end) : 'YYYY/MM/DD';
|
||||
|
||||
String get startDateUnix =>
|
||||
dateRange?.start != null
|
||||
? (dateRange!.start.millisecondsSinceEpoch ~/ 1000).toString()
|
||||
: '';
|
||||
|
||||
String get endDateUnix =>
|
||||
dateRange?.end != null
|
||||
? (dateRange!.end.millisecondsSinceEpoch ~/ 1000).toString()
|
||||
: '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
dateRange = widget.viewModel.selectedDateRange;
|
||||
status = _mapApiValueToDisplayString(widget.viewModel.selectedStatus);
|
||||
}
|
||||
|
||||
String _mapApiValueToDisplayString(String apiValue) {
|
||||
final mapping = {
|
||||
'All': TendersStrings.all,
|
||||
'Read': TendersStrings.read,
|
||||
'Unread': TendersStrings.unread,
|
||||
};
|
||||
return mapping[apiValue] ?? TendersStrings.all;
|
||||
}
|
||||
|
||||
Future<void> pick() async {
|
||||
final picked = await showDateRangePicker(
|
||||
context: context,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2050),
|
||||
initialDateRange: dateRange,
|
||||
);
|
||||
if (picked != null) {
|
||||
dateRange = picked;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<TendersViewModel>(
|
||||
builder: (context, vm, child) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
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(
|
||||
TendersStrings.selectDataRange,
|
||||
TendersStrings.filter,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0.sp(),
|
||||
fontWeight: FontWeight.w600,
|
||||
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,
|
||||
fontSize: 20.0.sp(),
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey70,
|
||||
),
|
||||
),
|
||||
...[
|
||||
TendersStrings.all,
|
||||
TendersStrings.read,
|
||||
TendersStrings.unread,
|
||||
].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,
|
||||
),
|
||||
const Spacer(),
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: SvgPicture.asset(
|
||||
AssetsManager.closeCircle,
|
||||
colorFilter: ColorFilter.mode(
|
||||
AppColors.grey80,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
);
|
||||
}),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
width: 24.0.w(),
|
||||
height: 24.0.h(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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_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';
|
||||
@@ -10,91 +9,108 @@ import 'package:tm_app/views/tenders/strings/tenders_strings.dart';
|
||||
|
||||
import '../../../view_models/tenders_view_model.dart';
|
||||
|
||||
class TendersSortDialog extends StatelessWidget {
|
||||
const TendersSortDialog({super.key});
|
||||
class TendersSortDialog extends StatefulWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<TendersViewModel>(
|
||||
builder: (context, vm, child) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
TendersStrings.sort,
|
||||
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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
TendersStrings.sort,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0.sp(),
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey70,
|
||||
),
|
||||
),
|
||||
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: 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,
|
||||
),
|
||||
const Spacer(),
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: SvgPicture.asset(
|
||||
AssetsManager.closeCircle,
|
||||
colorFilter: ColorFilter.mode(
|
||||
AppColors.grey80,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
);
|
||||
}),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
width: 24.0.w(),
|
||||
height: 24.0.h(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user