diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 76c576e..d49d3d5 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -10,6 +10,7 @@ import 'package:worker/features/cart/presentation/pages/cart_page.dart'; import 'package:worker/features/favorites/presentation/pages/favorites_page.dart'; import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart'; import 'package:worker/features/main/presentation/pages/main_scaffold.dart'; +import 'package:worker/features/orders/presentation/pages/orders_page.dart'; import 'package:worker/features/products/presentation/pages/product_detail_page.dart'; import 'package:worker/features/products/presentation/pages/products_page.dart'; import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart'; @@ -106,6 +107,16 @@ class AppRouter { ), ), + // Orders Route + GoRoute( + path: RouteNames.orders, + name: RouteNames.orders, + pageBuilder: (context, state) => MaterialPage( + key: state.pageKey, + child: const OrdersPage(), + ), + ), + // TODO: Add more routes as features are implemented ], diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index f33b8aa..47673f9 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -188,7 +188,7 @@ class HomePage extends ConsumerWidget { QuickAction( icon: Icons.inventory_2, label: 'Đơn hàng', - onTap: () => _showComingSoon(context, 'Đơn hàng', l10n), + onTap: () => context.push(RouteNames.orders), ), QuickAction( icon: Icons.receipt_long, diff --git a/lib/features/orders/data/datasources/orders_local_datasource.dart b/lib/features/orders/data/datasources/orders_local_datasource.dart new file mode 100644 index 0000000..012fa4e --- /dev/null +++ b/lib/features/orders/data/datasources/orders_local_datasource.dart @@ -0,0 +1,215 @@ +/// Local Data Source: Orders +/// +/// Provides mock order data for development and testing. +library; + +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:worker/core/database/models/enums.dart'; +import 'package:worker/features/orders/data/models/order_model.dart'; + +/// Orders Local Data Source +/// +/// Manages local mock order data. +class OrdersLocalDataSource { + /// Get all mock orders + Future> getAllOrders() async { + try { + debugPrint('[OrdersLocalDataSource] Loading mock orders...'); + + // Parse mock JSON data + final decoded = jsonDecode(_mockOrdersJson); + if (decoded is! List) { + throw Exception('Invalid JSON format: expected List'); + } + + final orders = (decoded as List) + .map((json) => OrderModel.fromJson(json as Map)) + .toList(); + + debugPrint('[OrdersLocalDataSource] Loaded ${orders.length} orders'); + return orders; + } catch (e, stackTrace) { + debugPrint('[OrdersLocalDataSource] Error loading orders: $e'); + debugPrint('Stack trace: $stackTrace'); + rethrow; + } + } + + /// Get orders by status + Future> getOrdersByStatus(OrderStatus status) async { + try { + final allOrders = await getAllOrders(); + final filtered = allOrders + .where((order) => order.status == status) + .toList(); + + debugPrint( + '[OrdersLocalDataSource] Filtered ${filtered.length} orders with status: $status', + ); + return filtered; + } catch (e) { + debugPrint('[OrdersLocalDataSource] Error filtering orders: $e'); + rethrow; + } + } + + /// Search orders by order number + Future> searchOrders(String query) async { + try { + if (query.isEmpty) { + return getAllOrders(); + } + + final allOrders = await getAllOrders(); + final filtered = allOrders + .where( + (order) => + order.orderNumber.toLowerCase().contains(query.toLowerCase()), + ) + .toList(); + + debugPrint( + '[OrdersLocalDataSource] Found ${filtered.length} orders matching "$query"', + ); + return filtered; + } catch (e) { + debugPrint('[OrdersLocalDataSource] Error searching orders: $e'); + rethrow; + } + } + + /// Get order by ID + Future getOrderById(String orderId) async { + try { + final allOrders = await getAllOrders(); + final order = allOrders.firstWhere( + (order) => order.orderId == orderId, + orElse: () => throw Exception('Order not found: $orderId'), + ); + + debugPrint('[OrdersLocalDataSource] Found order: ${order.orderNumber}'); + return order; + } catch (e) { + debugPrint('[OrdersLocalDataSource] Error getting order: $e'); + return null; + } + } + + /// Mock orders JSON data + /// Matches the HTML design with 5 sample orders + static const String _mockOrdersJson = ''' + [ + { + "order_id": "ord_001", + "order_number": "DH001234", + "user_id": "user_001", + "status": "processing", + "total_amount": 12900000, + "discount_amount": 0, + "tax_amount": 0, + "shipping_fee": 0, + "final_amount": 12900000, + "shipping_address": { + "name": "Nguyễn Văn A", + "phone": "0901234567", + "street": "123 Đường Nguyễn Văn Linh", + "district": "Quận 7", + "city": "HCM", + "postal_code": "70000" + }, + "expected_delivery_date": "2025-08-06T00:00:00.000Z", + "created_at": "2025-08-03T00:00:00.000Z", + "updated_at": "2025-08-03T00:00:00.000Z" + }, + { + "order_id": "ord_002", + "order_number": "DH001233", + "user_id": "user_001", + "status": "completed", + "total_amount": 8500000, + "discount_amount": 0, + "tax_amount": 0, + "shipping_fee": 0, + "final_amount": 8500000, + "shipping_address": { + "name": "Trần Thị B", + "phone": "0912345678", + "street": "456 Đại lộ Bình Dương", + "city": "Thủ Dầu Một, Bình Dương", + "postal_code": "75000" + }, + "expected_delivery_date": "2025-06-27T00:00:00.000Z", + "actual_delivery_date": "2025-06-27T00:00:00.000Z", + "created_at": "2025-06-24T00:00:00.000Z", + "updated_at": "2025-06-27T00:00:00.000Z" + }, + { + "order_id": "ord_003", + "order_number": "DH001232", + "user_id": "user_001", + "status": "shipped", + "total_amount": 15200000, + "discount_amount": 0, + "tax_amount": 0, + "shipping_fee": 0, + "final_amount": 15200000, + "shipping_address": { + "name": "Lê Văn C", + "phone": "0923456789", + "street": "789 Phố Duy Tân", + "district": "Cầu Giấy", + "city": "Hà Nội", + "postal_code": "10000" + }, + "expected_delivery_date": "2025-03-05T00:00:00.000Z", + "created_at": "2025-03-01T00:00:00.000Z", + "updated_at": "2025-03-02T00:00:00.000Z" + }, + { + "order_id": "ord_004", + "order_number": "DH001231", + "user_id": "user_001", + "status": "pending", + "total_amount": 6750000, + "discount_amount": 0, + "tax_amount": 0, + "shipping_fee": 0, + "final_amount": 6750000, + "shipping_address": { + "name": "Phạm Thị D", + "phone": "0934567890", + "street": "321 Đường Võ Văn Ngân", + "city": "Thủ Đức, HCM", + "postal_code": "71000" + }, + "expected_delivery_date": "2024-11-12T00:00:00.000Z", + "created_at": "2024-11-08T00:00:00.000Z", + "updated_at": "2024-11-08T00:00:00.000Z" + }, + { + "order_id": "ord_005", + "order_number": "DH001230", + "user_id": "user_001", + "status": "cancelled", + "total_amount": 3200000, + "discount_amount": 0, + "tax_amount": 0, + "shipping_fee": 0, + "final_amount": 3200000, + "shipping_address": { + "name": "Hoàng Văn E", + "phone": "0945678901", + "street": "654 Đường 3 Tháng 2", + "city": "Rạch Giá, Kiên Giang", + "postal_code": "92000" + }, + "expected_delivery_date": "2024-08-04T00:00:00.000Z", + "cancellation_reason": "Khách hàng yêu cầu hủy", + "created_at": "2024-07-30T00:00:00.000Z", + "updated_at": "2024-07-31T00:00:00.000Z" + } + ] + '''; +} diff --git a/lib/features/orders/presentation/pages/orders_page.dart b/lib/features/orders/presentation/pages/orders_page.dart new file mode 100644 index 0000000..5f72961 --- /dev/null +++ b/lib/features/orders/presentation/pages/orders_page.dart @@ -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 createState() => _OrdersPageState(); +} + +class _OrdersPageState extends ConsumerState { + 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, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/orders/presentation/providers/orders_provider.dart b/lib/features/orders/presentation/providers/orders_provider.dart new file mode 100644 index 0000000..f493ef3 --- /dev/null +++ b/lib/features/orders/presentation/providers/orders_provider.dart @@ -0,0 +1,154 @@ +/// Providers: Orders +/// +/// Riverpod providers for managing orders state. +library; + +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:worker/core/database/models/enums.dart'; +import 'package:worker/features/orders/data/datasources/orders_local_datasource.dart'; +import 'package:worker/features/orders/data/models/order_model.dart'; + +part 'orders_provider.g.dart'; + +/// Orders Local Data Source Provider +@riverpod +OrdersLocalDataSource ordersLocalDataSource(Ref ref) { + return OrdersLocalDataSource(); +} + +/// Orders Provider +/// +/// Provides list of all orders from local data source. +@riverpod +class Orders extends _$Orders { + @override + Future> build() async { + return await ref.read(ordersLocalDataSourceProvider).getAllOrders(); + } + + /// Refresh orders + Future refresh() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + return await ref.read(ordersLocalDataSourceProvider).getAllOrders(); + }); + } +} + +/// Selected Order Status Provider +/// +/// Tracks the currently selected order status filter. +/// null means "All" orders. +@riverpod +class SelectedOrderStatus extends _$SelectedOrderStatus { + @override + OrderStatus? build() { + return null; // Default: show all orders + } + + /// Select a status filter + void selectStatus(OrderStatus? status) { + state = status; + } + + /// Clear selection (show all) + void clearSelection() { + state = null; + } +} + +/// Order Search Query Provider +/// +/// Tracks the current search query for order numbers. +@riverpod +class OrderSearchQuery extends _$OrderSearchQuery { + @override + String build() { + return ''; + } + + /// Update search query + void updateQuery(String query) { + state = query; + } + + /// Clear search query + void clearQuery() { + state = ''; + } +} + +/// Filtered Orders Provider +/// +/// Filters orders by selected status and search query. +@riverpod +Future> filteredOrders(Ref ref) async { + final ordersAsync = ref.watch(ordersProvider); + final selectedStatus = ref.watch(selectedOrderStatusProvider); + final searchQuery = ref.watch(orderSearchQueryProvider); + + return ordersAsync.when( + data: (orders) { + var filtered = orders; + + // Filter by status + if (selectedStatus != null) { + filtered = filtered + .where((order) => order.status == selectedStatus) + .toList(); + } + + // Filter by search query + if (searchQuery.isNotEmpty) { + filtered = filtered + .where( + (order) => order.orderNumber + .toLowerCase() + .contains(searchQuery.toLowerCase()), + ) + .toList(); + } + + // Sort by creation date (newest first) + filtered.sort((a, b) => b.createdAt.compareTo(a.createdAt)); + + return filtered; + }, + loading: () => [], + error: (error, stack) => [], + ); +} + +/// Orders Count by Status Provider +/// +/// Returns count of orders for each status. +@riverpod +Future> ordersCountByStatus(Ref ref) async { + final ordersAsync = ref.watch(ordersProvider); + + return ordersAsync.when( + data: (orders) { + final counts = {}; + + for (final status in OrderStatus.values) { + counts[status] = orders.where((order) => order.status == status).length; + } + + return counts; + }, + loading: () => {}, + error: (error, stack) => {}, + ); +} + +/// Total Orders Count Provider +@riverpod +Future totalOrdersCount(Ref ref) async { + final ordersAsync = ref.watch(ordersProvider); + + return ordersAsync.when( + data: (orders) => orders.length, + loading: () => 0, + error: (error, stack) => 0, + ); +} diff --git a/lib/features/orders/presentation/providers/orders_provider.g.dart b/lib/features/orders/presentation/providers/orders_provider.g.dart new file mode 100644 index 0000000..944e5a7 --- /dev/null +++ b/lib/features/orders/presentation/providers/orders_provider.g.dart @@ -0,0 +1,402 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'orders_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Orders Local Data Source Provider + +@ProviderFor(ordersLocalDataSource) +const ordersLocalDataSourceProvider = OrdersLocalDataSourceProvider._(); + +/// Orders Local Data Source Provider + +final class OrdersLocalDataSourceProvider + extends + $FunctionalProvider< + OrdersLocalDataSource, + OrdersLocalDataSource, + OrdersLocalDataSource + > + with $Provider { + /// Orders Local Data Source Provider + const OrdersLocalDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'ordersLocalDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$ordersLocalDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + OrdersLocalDataSource create(Ref ref) { + return ordersLocalDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(OrdersLocalDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$ordersLocalDataSourceHash() => + r'5fb1c0b212ea6874bd42ae1f5b3f9f1db7197d7b'; + +/// Orders Provider +/// +/// Provides list of all orders from local data source. + +@ProviderFor(Orders) +const ordersProvider = OrdersProvider._(); + +/// Orders Provider +/// +/// Provides list of all orders from local data source. +final class OrdersProvider + extends $AsyncNotifierProvider> { + /// Orders Provider + /// + /// Provides list of all orders from local data source. + const OrdersProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'ordersProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$ordersHash(); + + @$internal + @override + Orders create() => Orders(); +} + +String _$ordersHash() => r'7d2ae33e528260172495e8360f6879cb6e089766'; + +/// Orders Provider +/// +/// Provides list of all orders from local data source. + +abstract class _$Orders extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = + this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// Selected Order Status Provider +/// +/// Tracks the currently selected order status filter. +/// null means "All" orders. + +@ProviderFor(SelectedOrderStatus) +const selectedOrderStatusProvider = SelectedOrderStatusProvider._(); + +/// Selected Order Status Provider +/// +/// Tracks the currently selected order status filter. +/// null means "All" orders. +final class SelectedOrderStatusProvider + extends $NotifierProvider { + /// Selected Order Status Provider + /// + /// Tracks the currently selected order status filter. + /// null means "All" orders. + const SelectedOrderStatusProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'selectedOrderStatusProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$selectedOrderStatusHash(); + + @$internal + @override + SelectedOrderStatus create() => SelectedOrderStatus(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(OrderStatus? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$selectedOrderStatusHash() => + r'51834a8660a7f792e4075f76354e8a23a4fe9d7c'; + +/// Selected Order Status Provider +/// +/// Tracks the currently selected order status filter. +/// null means "All" orders. + +abstract class _$SelectedOrderStatus extends $Notifier { + OrderStatus? build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + OrderStatus?, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// Order Search Query Provider +/// +/// Tracks the current search query for order numbers. + +@ProviderFor(OrderSearchQuery) +const orderSearchQueryProvider = OrderSearchQueryProvider._(); + +/// Order Search Query Provider +/// +/// Tracks the current search query for order numbers. +final class OrderSearchQueryProvider + extends $NotifierProvider { + /// Order Search Query Provider + /// + /// Tracks the current search query for order numbers. + const OrderSearchQueryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'orderSearchQueryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$orderSearchQueryHash(); + + @$internal + @override + OrderSearchQuery create() => OrderSearchQuery(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(String value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$orderSearchQueryHash() => r'4f60c2a1ac8c0ce515bc807781fa3813e9934482'; + +/// Order Search Query Provider +/// +/// Tracks the current search query for order numbers. + +abstract class _$OrderSearchQuery extends $Notifier { + String build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + String, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// Filtered Orders Provider +/// +/// Filters orders by selected status and search query. + +@ProviderFor(filteredOrders) +const filteredOrdersProvider = FilteredOrdersProvider._(); + +/// Filtered Orders Provider +/// +/// Filters orders by selected status and search query. + +final class FilteredOrdersProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with $FutureModifier>, $FutureProvider> { + /// Filtered Orders Provider + /// + /// Filters orders by selected status and search query. + const FilteredOrdersProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'filteredOrdersProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$filteredOrdersHash(); + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + return filteredOrders(ref); + } +} + +String _$filteredOrdersHash() => r'3bef3cc2b8c98297510134d10ceb5ef1618cc3a8'; + +/// Orders Count by Status Provider +/// +/// Returns count of orders for each status. + +@ProviderFor(ordersCountByStatus) +const ordersCountByStatusProvider = OrdersCountByStatusProvider._(); + +/// Orders Count by Status Provider +/// +/// Returns count of orders for each status. + +final class OrdersCountByStatusProvider + extends + $FunctionalProvider< + AsyncValue>, + Map, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + /// Orders Count by Status Provider + /// + /// Returns count of orders for each status. + const OrdersCountByStatusProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'ordersCountByStatusProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$ordersCountByStatusHash(); + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + return ordersCountByStatus(ref); + } +} + +String _$ordersCountByStatusHash() => + r'3a656b9c70510f34702a636b9bda5a7b7660e6ff'; + +/// Total Orders Count Provider + +@ProviderFor(totalOrdersCount) +const totalOrdersCountProvider = TotalOrdersCountProvider._(); + +/// Total Orders Count Provider + +final class TotalOrdersCountProvider + extends $FunctionalProvider, int, FutureOr> + with $FutureModifier, $FutureProvider { + /// Total Orders Count Provider + const TotalOrdersCountProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'totalOrdersCountProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$totalOrdersCountHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return totalOrdersCount(ref); + } +} + +String _$totalOrdersCountHash() => r'71f2e2f890b4e3d5a5beaa7bf06a0d78faf039cb'; diff --git a/lib/features/orders/presentation/widgets/order_card.dart b/lib/features/orders/presentation/widgets/order_card.dart new file mode 100644 index 0000000..2275089 --- /dev/null +++ b/lib/features/orders/presentation/widgets/order_card.dart @@ -0,0 +1,249 @@ +/// Widget: Order Card +/// +/// Displays order information in a card format. +library; + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:worker/core/database/models/enums.dart'; +import 'package:worker/core/theme/colors.dart'; +import 'package:worker/features/orders/data/models/order_model.dart'; + +/// Order Card Widget +/// +/// Displays order details in a card with status indicator. +class OrderCard extends StatelessWidget { + /// Order to display + final OrderModel order; + + /// Tap callback + final VoidCallback? onTap; + + const OrderCard({ + required this.order, + this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + final currencyFormatter = NumberFormat.currency( + locale: 'vi_VN', + symbol: 'đ', + decimalDigits: 0, + ); + + return Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: _getStatusColor(order.status), + width: 4, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Order number and amount row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Order number + Text( + '#${order.orderNumber}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + + // Amount + Text( + currencyFormatter.format(order.finalAmount), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.primaryBlue, + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Order details + _buildDetailRow( + 'Ngày đặt:', + _formatDate(order.createdAt), + ), + const SizedBox(height: 6), + + _buildDetailRow( + 'Ngày giao:', + order.expectedDeliveryDate != null + ? _formatDate(order.expectedDeliveryDate!) + : 'Chưa xác định', + ), + const SizedBox(height: 6), + + _buildDetailRow( + 'Địa chỉ:', + _getShortAddress(), + ), + const SizedBox(height: 12), + + // Status badge + _buildStatusBadge(), + ], + ), + ), + ), + ), + ); + } + + /// Build detail row + Widget _buildDetailRow(String label, String value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey500, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + value, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey900, + ), + ), + ), + ], + ); + } + + /// Build status badge + Widget _buildStatusBadge() { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: _getStatusColor(order.status).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: _getStatusColor(order.status).withValues(alpha: 0.3), + width: 1, + ), + ), + child: Text( + _getStatusText(order.status), + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: _getStatusColor(order.status), + ), + ), + ); + } + + /// Get status color + Color _getStatusColor(OrderStatus status) { + switch (status) { + case OrderStatus.draft: + return AppColors.grey500; + case OrderStatus.pending: + return const Color(0xFFF59E0B); // warning/pending color + case OrderStatus.confirmed: + return const Color(0xFFF59E0B); // warning/pending color + case OrderStatus.processing: + return AppColors.info; + case OrderStatus.shipped: + return const Color(0xFF3B82F6); // blue + case OrderStatus.delivered: + return const Color(0xFF10B981); // green + case OrderStatus.completed: + return AppColors.success; + case OrderStatus.cancelled: + return AppColors.danger; + case OrderStatus.refunded: + return const Color(0xFFF97316); // orange + } + } + + /// Get status text in Vietnamese + String _getStatusText(OrderStatus status) { + switch (status) { + case OrderStatus.draft: + return 'Nháp'; + case OrderStatus.pending: + return 'Chờ xác nhận'; + case OrderStatus.confirmed: + return 'Đã xác nhận'; + case OrderStatus.processing: + return 'Đang xử lý'; + case OrderStatus.shipped: + return 'Đang giao'; + case OrderStatus.delivered: + return 'Đã giao'; + case OrderStatus.completed: + return 'Hoàn thành'; + case OrderStatus.cancelled: + return 'Đã hủy'; + case OrderStatus.refunded: + return 'Đã hoàn tiền'; + } + } + + /// Format date to dd/MM/yyyy + String _formatDate(DateTime date) { + return DateFormat('dd/MM/yyyy').format(date); + } + + /// Get short address (city or district, city) + String _getShortAddress() { + if (order.shippingAddress == null) { + return 'Chưa có địa chỉ'; + } + + try { + final addressJson = jsonDecode(order.shippingAddress!); + final city = addressJson['city'] as String?; + final district = addressJson['district'] as String?; + + if (district != null && city != null) { + return '$district, $city'; + } else if (city != null) { + return city; + } else { + return 'Chưa có địa chỉ'; + } + } catch (e) { + return 'Chưa có địa chỉ'; + } + } +}