orders
This commit is contained in:
376
lib/features/orders/presentation/pages/orders_page.dart
Normal file
376
lib/features/orders/presentation/pages/orders_page.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user