This commit is contained in:
Phuoc Nguyen
2025-10-27 09:33:00 +07:00
parent 380ad60ee5
commit 830ef7e2a2
7 changed files with 1408 additions and 1 deletions

View File

@@ -0,0 +1,376 @@
/// Page: Orders Page
///
/// Displays list of orders with search and filter functionality.
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/database/models/enums.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/orders/presentation/providers/orders_provider.dart';
import 'package:worker/features/orders/presentation/widgets/order_card.dart';
/// Orders Page
///
/// Features:
/// - Search bar for order numbers
/// - Filter pills for order status
/// - List of order cards
/// - Pull-to-refresh
class OrdersPage extends ConsumerStatefulWidget {
const OrdersPage({super.key});
@override
ConsumerState<OrdersPage> createState() => _OrdersPageState();
}
class _OrdersPageState extends ConsumerState<OrdersPage> {
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.removeListener(_onSearchChanged);
_searchController.dispose();
super.dispose();
}
void _onSearchChanged() {
ref.read(orderSearchQueryProvider.notifier).updateQuery(
_searchController.text,
);
}
@override
Widget build(BuildContext context) {
final filteredOrdersAsync = ref.watch(filteredOrdersProvider);
final selectedStatus = ref.watch(selectedOrderStatusProvider);
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => context.pop(),
),
title: const Text(
'Danh sách đơn hàng',
style: TextStyle(color: Colors.black),
),
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
centerTitle: false,
actions: const [
SizedBox(width: AppSpacing.sm),
],
),
body: RefreshIndicator(
onRefresh: () async {
await ref.read(ordersProvider.notifier).refresh();
},
child: CustomScrollView(
slivers: [
// Search Bar
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: _buildSearchBar(),
),
),
// Filter Pills
SliverToBoxAdapter(
child: _buildFilterPills(selectedStatus),
),
// Orders List
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
sliver: filteredOrdersAsync.when(
data: (orders) {
if (orders.isEmpty) {
return _buildEmptyState();
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final order = orders[index];
return OrderCard(
order: order,
onTap: () {
// TODO: Navigate to order detail page
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Order ${order.orderNumber} tapped',
),
duration: const Duration(seconds: 1),
),
);
},
);
},
childCount: orders.length,
),
);
},
loading: () => _buildLoadingState(),
error: (error, stack) => _buildErrorState(error),
),
),
],
),
),
);
}
/// Build search bar
Widget _buildSearchBar() {
return Container(
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Mã đơn hàng',
hintStyle: const TextStyle(
color: AppColors.grey500,
fontSize: 14,
),
prefixIcon: const Icon(
Icons.search,
color: AppColors.grey500,
size: 20,
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(
Icons.clear,
color: AppColors.grey500,
size: 20,
),
onPressed: () {
_searchController.clear();
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
);
}
/// Build filter pills
Widget _buildFilterPills(OrderStatus? selectedStatus) {
return Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ListView(
scrollDirection: Axis.horizontal,
children: [
// All filter
_buildFilterChip(
label: 'Tất cả',
isSelected: selectedStatus == null,
onTap: () {
ref.read(selectedOrderStatusProvider.notifier).clearSelection();
},
),
const SizedBox(width: 8),
// Pending filter
_buildFilterChip(
label: 'Chờ xác nhận',
isSelected: selectedStatus == OrderStatus.pending,
onTap: () {
ref
.read(selectedOrderStatusProvider.notifier)
.selectStatus(OrderStatus.pending);
},
),
const SizedBox(width: 8),
// Processing filter
_buildFilterChip(
label: 'Đang xử lý',
isSelected: selectedStatus == OrderStatus.processing,
onTap: () {
ref
.read(selectedOrderStatusProvider.notifier)
.selectStatus(OrderStatus.processing);
},
),
const SizedBox(width: 8),
// Shipped filter
_buildFilterChip(
label: 'Đang giao',
isSelected: selectedStatus == OrderStatus.shipped,
onTap: () {
ref
.read(selectedOrderStatusProvider.notifier)
.selectStatus(OrderStatus.shipped);
},
),
const SizedBox(width: 8),
// Completed filter
_buildFilterChip(
label: 'Hoàn thành',
isSelected: selectedStatus == OrderStatus.completed,
onTap: () {
ref
.read(selectedOrderStatusProvider.notifier)
.selectStatus(OrderStatus.completed);
},
),
const SizedBox(width: 8),
// Cancelled filter
_buildFilterChip(
label: 'Đã hủy',
isSelected: selectedStatus == OrderStatus.cancelled,
onTap: () {
ref
.read(selectedOrderStatusProvider.notifier)
.selectStatus(OrderStatus.cancelled);
},
),
],
),
);
}
/// Build individual filter chip
Widget _buildFilterChip({
required String label,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? AppColors.primaryBlue : AppColors.grey100,
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected ? Colors.white : AppColors.grey900,
),
),
),
),
);
}
/// Build empty state
Widget _buildEmptyState() {
return SliverFillRemaining(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.receipt_long_outlined,
size: 80,
color: AppColors.grey500.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
const Text(
'Không có đơn hàng nào',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.grey500,
),
),
const SizedBox(height: 8),
const Text(
'Thử tìm kiếm với từ khóa khác',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
),
);
}
/// Build loading state
Widget _buildLoadingState() {
return const SliverFillRemaining(
child: Center(
child: CircularProgressIndicator(),
),
);
}
/// Build error state
Widget _buildErrorState(Object error) {
return SliverFillRemaining(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 80,
color: AppColors.danger.withValues(alpha: 0.7),
),
const SizedBox(height: 16),
const Text(
'Có lỗi xảy ra',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 8),
Text(
error.toString(),
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}