Merge pull request 'dialogs_fix' (#212) from dialogs_fix into main

Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/212
This commit is contained in:
a.ghabeli
2025-10-12 15:30:34 +03:30
17 changed files with 643 additions and 438 deletions
@@ -3,7 +3,8 @@ enum TenderApprovalStatus {
submitted,
rejected,
unsubmitted,
unrejected;
unrejected,
approved;
/// Convert enum to string value for API
String get value {
@@ -16,6 +17,8 @@ enum TenderApprovalStatus {
return 'unsubmitted';
case TenderApprovalStatus.unrejected:
return 'unrejected';
case TenderApprovalStatus.approved:
return 'approved';
}
}
}
+3 -2
View File
@@ -212,11 +212,12 @@ class NotificationRouteData extends GoRouteData with _$NotificationRouteData {
@TypedGoRoute<YourTendersRouteData>(path: '/your-tenders')
class YourTendersRouteData extends GoRouteData with _$YourTendersRouteData {
const YourTendersRouteData();
final String status;
const YourTendersRouteData({required this.status});
@override
Widget build(BuildContext context, GoRouterState state) {
return yourTendersProvider(child: const YourTendersScreen());
return yourTendersProvider(child: YourTendersScreen(status: status));
}
}
+7 -2
View File
@@ -247,10 +247,15 @@ RouteBase get $yourTendersRouteData => GoRouteData.$route(
mixin _$YourTendersRouteData on GoRouteData {
static YourTendersRouteData _fromState(GoRouterState state) =>
const YourTendersRouteData();
YourTendersRouteData(status: state.uri.queryParameters['status']!);
YourTendersRouteData get _self => this as YourTendersRouteData;
@override
String get location => GoRouteData.$location('/your-tenders');
String get location => GoRouteData.$location(
'/your-tenders',
queryParams: {'status': _self.status},
);
@override
void go(BuildContext context) => context.go(location);
+1 -1
View File
@@ -8,7 +8,7 @@ class YourTendersApi {
required String createdTo,
required String status,
}) {
String url = '$tenderApproval?limit=$limit&offset=$offset&status=submitted';
String url = '$tenderApproval?limit=$limit&offset=$offset';
if (createdFrom.isNotEmpty && createdTo.isNotEmpty) {
url += '&created_from=$createdFrom&created_to=$createdTo';
+8 -3
View File
@@ -279,7 +279,7 @@ class YourTendersViewModel with ChangeNotifier {
offset: _tendersOffset,
);
await getTenders(requestModel: requestModel, reset: true);
} else if (selectedStatus == YourTendersStrings.tendersSubmitted) {
} else if (selectedStatus == TenderApprovalStatus.submitted.value) {
final requestModel = GetTendersRequestModel(
status: TenderApprovalStatus.submitted.value,
createdFrom: startDateUnix,
@@ -288,16 +288,21 @@ class YourTendersViewModel with ChangeNotifier {
offset: _tendersOffset,
);
await getTenders(requestModel: requestModel, reset: true);
} else if (selectedStatus == YourTendersStrings.likeTenders) {
} else if (selectedStatus == TenderFeedback.liked.value) {
await getTendersFeedback(
feedbackType: TenderFeedback.liked.value,
reset: true,
);
} else if (selectedStatus == YourTendersStrings.dislikeTenders) {
} else if (selectedStatus == TenderFeedback.disliked.value) {
await getTendersFeedback(
feedbackType: TenderFeedback.disliked.value,
reset: true,
);
} else if (selectedStatus == TenderApprovalStatus.approved.value) {
await getTendersFeedback(
feedbackType: TenderApprovalStatus.approved.value,
reset: true,
);
}
}
+7 -2
View File
@@ -7,6 +7,7 @@ import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/home_view_model.dart';
import 'package:tm_app/views/shared/desktop_navigation_widget.dart';
import '../../../core/constants/tender_approval_status.dart';
import '../strings/home_strings.dart';
import '../widgets.dart';
@@ -138,7 +139,9 @@ class DesktopHomePage extends StatelessWidget {
width: 178,
height: 148,
onTap: () {
const YourTendersRouteData().push(context);
YourTendersRouteData(
status: TenderApprovalStatus.submitted.value,
).push(context);
},
),
),
@@ -154,7 +157,9 @@ class DesktopHomePage extends StatelessWidget {
width: 178,
height: 148,
onTap: () {
const YourTendersRouteData().push(context);
YourTendersRouteData(
status: TenderApprovalStatus.approved.value,
).push(context);
},
),
),
+7 -2
View File
@@ -8,6 +8,7 @@ import 'package:tm_app/data/services/model/tender_approvals_stats_response/tende
import 'package:tm_app/view_models/home_view_model.dart';
import 'package:tm_app/views/shared/tender_app_bar.dart';
import '../../../core/constants/tender_approval_status.dart';
import '../strings/home_strings.dart';
import '../widgets.dart';
@@ -108,7 +109,9 @@ class MobileHomePage extends StatelessWidget {
textColor: AppColors.mainBlue,
enableTap: true,
onTap: () {
const YourTendersRouteData().push(context);
YourTendersRouteData(
status: TenderApprovalStatus.submitted.value,
).push(context);
},
),
TenderCard(
@@ -120,7 +123,9 @@ class MobileHomePage extends StatelessWidget {
textColor: AppColors.jellyBean,
enableTap: true,
onTap: () {
const YourTendersRouteData().push(context);
YourTendersRouteData(
status: TenderApprovalStatus.approved.value,
).push(context);
},
),
],
+7 -2
View File
@@ -8,6 +8,7 @@ import 'package:tm_app/view_models/home_view_model.dart';
import 'package:tm_app/views/shared/tablet_navigation_widget.dart';
import '../../../core/constants/assets.dart';
import '../../../core/constants/tender_approval_status.dart';
import '../../shared/tender_app_bar.dart';
import '../strings/home_strings.dart';
import '../widgets.dart';
@@ -130,7 +131,9 @@ class TabletHomePage extends StatelessWidget {
width: double.infinity,
height: 148,
onTap: () {
const YourTendersRouteData().push(context);
YourTendersRouteData(
status: TenderApprovalStatus.submitted.value,
).push(context);
},
),
),
@@ -146,7 +149,9 @@ class TabletHomePage extends StatelessWidget {
width: double.infinity,
height: 148,
onTap: () {
const YourTendersRouteData().push(context);
YourTendersRouteData(
status: TenderApprovalStatus.approved.value,
).push(context);
},
),
),
+18 -4
View File
@@ -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,
),
),
],
),
],
),
),
);
}
}
@@ -61,7 +61,7 @@ class _YourTendersMobilePageState extends State<YourTendersMobilePage> {
return Column(
children: [
if (viewModel.selectedStatus ==
YourTendersStrings.likeTenders &&
TenderFeedback.liked.value &&
viewModel.likedTendersData != null)
Expanded(
child: LikedDislikedTendersList(
@@ -72,7 +72,7 @@ class _YourTendersMobilePageState extends State<YourTendersMobilePage> {
),
)
else if (viewModel.selectedStatus ==
YourTendersStrings.dislikeTenders &&
TenderFeedback.disliked.value &&
viewModel.likedTendersData != null)
Expanded(
child: LikedDislikedTendersList(
@@ -7,10 +7,9 @@ import 'package:tm_app/views/your_tenders/pages/your_tenders_desktop_page.dart';
import 'package:tm_app/views/your_tenders/pages/your_tenders_mobile_page.dart';
import 'package:tm_app/views/your_tenders/pages/your_tenders_tablet_page.dart';
import '../../../data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
class YourTendersScreen extends StatefulWidget {
const YourTendersScreen({super.key});
const YourTendersScreen({required this.status, super.key});
final String status;
@override
State<YourTendersScreen> createState() => _YourTendersScreenState();
@@ -23,8 +22,8 @@ class _YourTendersScreenState extends State<YourTendersScreen> {
super.initState();
viewModel = context.read<YourTendersViewModel>();
WidgetsBinding.instance.addPostFrameCallback((_) {
final requestModel = GetTendersRequestModel();
viewModel.getTenders(requestModel: requestModel, reset: true);
viewModel.setStatus(widget.status);
viewModel.callWithFilter();
});
}
@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
import 'package:tm_app/view_models/your_tenders_view_model.dart';
import 'package:tm_app/views/your_tenders/strings/your_tenders_strings.dart';
import 'your_tenders_filter_dialog.dart';
@@ -11,6 +14,7 @@ class FilterButton extends StatelessWidget {
const FilterButton({required this.platformType, super.key});
void _openFilter(BuildContext context) {
final viewModel = context.read<YourTendersViewModel>();
if (platformType == PlatformType.mobile) {
showModalBottomSheet(
context: context,
@@ -19,7 +23,24 @@ class FilterButton extends StatelessWidget {
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (_) => const YourTendersFilterDialog(),
builder:
(_) => YourTendersFilterDialog(
viewModel: viewModel,
onFilterChanged: (dateRange, status) {
viewModel.setDateRange(dateRange);
viewModel.setStatus(status!);
if (status == YourTendersStrings.all) {
GetTendersRequestModel requestModel = GetTendersRequestModel(
status: '',
createdFrom: viewModel.startDateUnix,
createdTo: viewModel.endDateUnix,
);
viewModel.getTenders(requestModel: requestModel);
} else {
viewModel.callWithFilter();
}
},
),
);
} else {
showDialog(
@@ -30,7 +51,24 @@ class FilterButton extends StatelessWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: const YourTendersFilterDialog(),
child: YourTendersFilterDialog(
viewModel: viewModel,
onFilterChanged: (dateRange, status) {
viewModel.setDateRange(dateRange);
viewModel.setStatus(status!);
if (status == YourTendersStrings.all) {
GetTendersRequestModel requestModel =
GetTendersRequestModel(
status: '',
createdFrom: viewModel.startDateUnix,
createdTo: viewModel.endDateUnix,
);
viewModel.getTenders(requestModel: requestModel);
} else {
viewModel.callWithFilter();
}
},
),
),
);
}
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/tender_approval_status.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';
@@ -57,12 +58,24 @@ class _TendersSubmittedState extends State<TendersSubmitted> {
return Consumer<YourTendersViewModel>(
builder: (context, viewModel, child) {
if (widget.approvedTenders.data == null) {
return const Center(child: Text('No submitted projects yet.'));
return Center(
child: Text(
viewModel.selectedStatus == TenderApprovalStatus.approved.value
? 'No Approved data.'
: 'No Submitted data.',
),
);
}
final tenders = widget.approvedTenders.data!.tenders ?? [];
if (tenders.isEmpty) {
return const Center(child: Text('No submitted projects yet.'));
return Center(
child: Text(
viewModel.selectedStatus == TenderApprovalStatus.approved.value
? 'No Approved data.'
: 'No Submitted data.',
),
);
}
final itemCount =
@@ -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';
@@ -9,171 +9,235 @@ import 'package:tm_app/view_models/your_tenders_view_model.dart';
import 'package:tm_app/views/shared/base_button.dart';
import 'package:tm_app/views/your_tenders/strings/your_tenders_strings.dart';
class YourTendersFilterDialog extends StatelessWidget {
const YourTendersFilterDialog({super.key});
import '../../../core/constants/tender_approval_status.dart';
import '../../../core/constants/tender_feedback.dart';
Widget _dateRangePill(BuildContext context) {
final viewModel = context.watch<YourTendersViewModel>();
final start = viewModel.selectedDateRange?.start;
final end = viewModel.selectedDateRange?.end;
class YourTendersFilterDialog extends StatefulWidget {
final YourTendersViewModel viewModel;
final Function(DateTimeRange? dateRange, String? status) onFilterChanged;
const YourTendersFilterDialog({
required this.viewModel,
required this.onFilterChanged,
super.key,
});
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);
}
}
@override
State<YourTendersFilterDialog> createState() =>
_YourTendersFilterDialogState();
}
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
? YourTendersStrings.startDate
: viewModel.startDateFormatted,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
Text(
end == null
? YourTendersStrings.endDate
: viewModel.endDateFormatted,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
],
),
],
),
),
),
class _YourTendersFilterDialogState extends State<YourTendersFilterDialog> {
DateTimeRange? dateRange;
String? status = YourTendersStrings.all;
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);
}
Future<void> pick() async {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2000),
lastDate: DateTime(2050),
initialDateRange: dateRange,
);
if (picked != null) {
dateRange = picked;
setState(() {});
}
}
void setStatus(String status) {
this.status = status;
setState(() {});
}
/// Maps API values to display strings for radio buttons
String _mapApiValueToDisplayString(String apiValue) {
final mapping = {
'All': YourTendersStrings.all,
'like': YourTendersStrings.likeTenders,
'dislike': YourTendersStrings.dislikeTenders,
'approved': YourTendersStrings.approvedTenders,
'submitted': YourTendersStrings.tendersSubmitted,
};
return mapping[apiValue] ?? YourTendersStrings.all;
}
/// Maps display strings to API values for the view model
String _mapDisplayStringToApiValue(String displayString) {
final mapping = {
YourTendersStrings.all: YourTendersStrings.all,
YourTendersStrings.likeTenders: TenderFeedback.liked.value,
YourTendersStrings.dislikeTenders: TenderFeedback.disliked.value,
YourTendersStrings.approvedTenders: TenderApprovalStatus.approved.value,
YourTendersStrings.tendersSubmitted: TenderApprovalStatus.submitted.value,
};
return mapping[displayString] ?? YourTendersStrings.all;
}
@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(
YourTendersStrings.filter,
style: TextStyle(
fontSize: 20.0.sp(),
fontWeight: FontWeight.w700,
color: AppColors.grey70,
),
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
YourTendersStrings.filter,
style: TextStyle(
fontSize: 20.0.sp(),
fontWeight: FontWeight.w700,
color: AppColors.grey70,
),
),
),
SizedBox(height: 16.0.h()),
Text(
YourTendersStrings.selectCreationRangeDate,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey80,
),
),
SizedBox(height: 20.0.h()),
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
? YourTendersStrings.startDate
: startDateFormatted,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
Text(
dateRange?.end == null
? YourTendersStrings.endDate
: endDateFormatted,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
],
),
],
),
),
SizedBox(height: 16.0.h()),
Text(
YourTendersStrings.selectCreationRangeDate,
),
),
SizedBox(height: 16.0.h()),
Text(
YourTendersStrings.status,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70,
),
),
...[
YourTendersStrings.all,
YourTendersStrings.likeTenders,
YourTendersStrings.dislikeTenders,
YourTendersStrings.approvedTenders,
YourTendersStrings.tendersSubmitted,
].map((status) {
return RadioListTile(
value: status,
groupValue: this.status,
activeColor: AppColors.mainBlue,
onChanged: (value) => setStatus(value!),
title: Text(
status,
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(
YourTendersStrings.status,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
...[
YourTendersStrings.all,
YourTendersStrings.likeTenders,
YourTendersStrings.dislikeTenders,
YourTendersStrings.approvedTenders,
YourTendersStrings.tendersSubmitted,
].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();
widget.onFilterChanged(
dateRange,
_mapDisplayStringToApiValue(status!),
);
}),
SizedBox(height: 16.0.h()),
SizedBox(
width: double.infinity,
child: BaseButton(
isEnabled: true,
onPressed: () {
context.pop();
vm.callWithFilter();
},
text: YourTendersStrings.confirm,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary20,
),
),
],
},
text: YourTendersStrings.confirm,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary20,
),
),
),
);
},
],
),
),
);
}
}
+2 -10
View File
@@ -1,5 +1,3 @@
////////////////////////////
import 'package:flutter_test/flutter_test.dart';
import 'package:tm_app/data/services/model/error/error_model.dart';
@@ -41,9 +39,7 @@ void main() {
details: 'Server error',
);
final updated = error.copyWith(
message: 'Timeout error',
);
final updated = error.copyWith(message: 'Timeout error');
expect(updated.message, 'Timeout error');
expect(updated.code, '500');
@@ -51,11 +47,7 @@ void main() {
});
test('fromJson handles null fields safely', () {
final json = {
'message': null,
'code': null,
'details': null,
};
final json = {'message': null, 'code': null, 'details': null};
final error = ErrorModel.fromJson(json);