feat: order range date filter

This commit is contained in:
efrilm
2025-08-19 11:42:26 +07:00
parent 590bb3329c
commit 5b91b5978f
9 changed files with 446 additions and 110 deletions
@@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -11,7 +13,7 @@ import '../../../components/button/button.dart';
import '../../../components/spacer/spacer.dart';
import '../../../components/widgets/empty_widget.dart';
import '../../../router/app_router.gr.dart';
import 'widgets/status_tile.dart';
import 'widgets/filter_header_delegate.dart';
import 'widgets/order_tile.dart';
@RoutePage()
@@ -38,7 +40,6 @@ class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
final ScrollController _scrollController = ScrollController();
// Filter state
String selectedFilter = 'All';
final List<String> filterOptions = ['All', 'Completed', 'Pending'];
@override
@@ -79,13 +80,27 @@ class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body: BlocListener<OrderLoaderBloc, OrderLoaderState>(
listenWhen: (p, c) => p.status != c.status,
listener: (context, state) {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(isRefresh: true),
);
},
body: MultiBlocListener(
listeners: [
BlocListener<OrderLoaderBloc, OrderLoaderState>(
listenWhen: (p, c) => p.status != c.status,
listener: (context, state) {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(isRefresh: true),
);
},
),
BlocListener<OrderLoaderBloc, OrderLoaderState>(
listenWhen: (previous, current) =>
previous.dateFrom != current.dateFrom ||
previous.dateTo != current.dateTo,
listener: (context, state) {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(isRefresh: true),
);
},
),
],
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
builder: (context, state) {
return NotificationListener<ScrollNotification>(
@@ -120,60 +135,36 @@ class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
// Pinned Filter Section
SliverPersistentHeader(
pinned: true,
delegate: _FilterHeaderDelegate(
child: Container(
color: AppColor.background,
padding: EdgeInsets.fromLTRB(
AppValue.padding,
10,
AppValue.padding,
10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: filterOptions.map((option) {
final index = filterOptions.indexOf(option);
return Padding(
padding: EdgeInsets.only(
right: index < filterOptions.length - 1
? 8
: 0,
),
child: OrderStatusTile(
label: option,
isSelected: option == selectedFilter,
onSelected: (isSelected) {
if (isSelected) {
setState(() {
selectedFilter = option;
});
if (option.toLowerCase() == 'all') {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.statusChanged(
'',
),
);
} else {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.statusChanged(
option.toLowerCase(),
),
);
}
}
},
),
);
}).toList(),
),
),
],
),
delegate: FilterHeaderDelegate(
backgroundColor: AppColor.background,
padding: EdgeInsets.fromLTRB(
AppValue.padding,
10,
AppValue.padding,
10,
),
startDate: state.dateFrom,
endDate: state.dateTo,
filterOptions: filterOptions,
selectedFilter: state.status,
onDateChanged: (startDate, endDate) {
if (startDate != null && endDate != null) {
log('Date changed');
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.rangeDateChanged(
startDate,
endDate,
),
);
}
},
onFilterChanged: (filter) {
final status = filter.toLowerCase();
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.statusChanged(status),
);
},
),
),
@@ -189,13 +180,13 @@ class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
child: Column(
children: [
// Show filtered transaction count
if (selectedFilter != 'All')
if (state.status != 'all')
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Text(
'${state.orders.length} ${selectedFilter.toLowerCase()} order${state.orders.length != 1 ? 's' : ''}',
'${state.orders.length} ${state.status.toLowerCase()} order${state.orders.length != 1 ? 's' : ''}',
style: TextStyle(
color: AppColor.textSecondary,
fontSize: 14,
@@ -210,7 +201,7 @@ class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
? EmptyWidget(
title: 'Order',
message:
'No ${selectedFilter.toLowerCase()} orders found',
'No ${state.status.toLowerCase()} orders found',
)
: ListView.builder(
itemCount: state.orders.length,
@@ -247,28 +238,3 @@ class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
}
// Custom delegate for pinned filter header
class _FilterHeaderDelegate extends SliverPersistentHeaderDelegate {
final Widget child;
_FilterHeaderDelegate({required this.child});
@override
double get minExtent => 70; // Minimum height when collapsed
@override
double get maxExtent => 70; // Maximum height when expanded
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return child;
}
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import '../../../../components/field/date_range_picker_field.dart';
import 'status_tile.dart';
class FilterHeaderDelegate extends SliverPersistentHeaderDelegate {
final Color backgroundColor;
final EdgeInsets padding;
final DateTime? startDate;
final DateTime? endDate;
final List<String> filterOptions;
final String selectedFilter;
final Function(DateTime?, DateTime?) onDateChanged;
final Function(String) onFilterChanged;
FilterHeaderDelegate({
required this.backgroundColor,
required this.padding,
required this.startDate,
required this.endDate,
required this.filterOptions,
required this.selectedFilter,
required this.onDateChanged,
required this.onFilterChanged,
});
@override
double get minExtent => 130;
@override
double get maxExtent => 130;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
print('FilterHeaderDelegate build called'); // Debug log
return Container(
color: backgroundColor,
padding: padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: DateRangePickerField(
maxDate: DateTime.now(),
startDate: startDate,
endDate: endDate,
onChanged: (start, end) {
print(
'onChanged called in FilterHeaderDelegate: $start - $end',
); // Debug log
onDateChanged(start, end);
},
),
),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: filterOptions.asMap().entries.map((entry) {
final index = entry.key;
final option = entry.value;
return Padding(
padding: EdgeInsets.only(
right: index < filterOptions.length - 1 ? 8 : 0,
),
child: OrderStatusTile(
label: option,
isSelected: option == selectedFilter,
onSelected: (isSelected) {
if (isSelected) {
onFilterChanged(option);
}
},
),
);
}).toList(),
),
),
),
],
),
);
}
@override
bool shouldRebuild(covariant FilterHeaderDelegate oldDelegate) {
return oldDelegate.startDate != startDate ||
oldDelegate.endDate != endDate ||
oldDelegate.selectedFilter != selectedFilter ||
oldDelegate.filterOptions.length != filterOptions.length ||
oldDelegate.backgroundColor != backgroundColor;
}
}