diff --git a/lib/core/constants/storage_constants.dart b/lib/core/constants/storage_constants.dart index 92a3ad2..81e7566 100644 --- a/lib/core/constants/storage_constants.dart +++ b/lib/core/constants/storage_constants.dart @@ -197,7 +197,50 @@ class HiveKeys { static const String lastSyncTime = 'last_sync_time'; static const String schemaVersion = 'schema_version'; static const String encryptionEnabled = 'encryption_enabled'; +} +/// Order Status Indices +/// +/// Index values for order statuses stored in Hive. +/// These correspond to the index field in OrderStatusModel. +/// Use these constants to compare order status by index instead of hardcoded strings. +/// +/// API Response Structure: +/// - status: "Pending approval" (English status name) +/// - label: "Chờ phê duyệt" (Vietnamese display label) +/// - color: "Warning" (Status color indicator) +/// - index: 1 (Unique identifier) +class OrderStatusIndex { + // Private constructor to prevent instantiation + OrderStatusIndex._(); + + /// Pending approval - "Chờ phê duyệt" + /// Color: Warning + static const int pendingApproval = 1; + + /// Manager Review - "Manager Review" + /// Color: Warning + static const int managerReview = 2; + + /// Processing - "Đang xử lý" + /// Color: Info + static const int processing = 3; + + /// Completed - "Hoàn thành" + /// Color: Success + static const int completed = 4; + + /// Rejected - "Từ chối" + /// Color: Danger + static const int rejected = 5; + + /// Cancelled - "HỦY BỎ" + /// Color: Danger + static const int cancelled = 6; +} + +/// Hive Keys (continued) +extension HiveKeysContinued on HiveKeys { // Cache Box Keys static const String productsCacheKey = 'products_cache'; static const String categoriesCacheKey = 'categories_cache'; diff --git a/lib/features/account/data/models/address_model.dart b/lib/features/account/data/models/address_model.dart index 5804d6a..8e73724 100644 --- a/lib/features/account/data/models/address_model.dart +++ b/lib/features/account/data/models/address_model.dart @@ -62,6 +62,10 @@ class AddressModel extends HiveObject { @HiveField(11) String? wardName; + /// Whether editing this address is allowed + @HiveField(12) + bool isAllowEdit; + AddressModel({ required this.name, required this.addressTitle, @@ -75,6 +79,7 @@ class AddressModel extends HiveObject { this.isDefault = false, this.cityName, this.wardName, + this.isAllowEdit = true, }); /// Create from JSON (API response) @@ -92,6 +97,7 @@ class AddressModel extends HiveObject { isDefault: json['is_default'] == 1 || json['is_default'] == true, cityName: json['city_name'] as String?, wardName: json['ward_name'] as String?, + isAllowEdit: json['is_allow_edit'] == 1 || json['is_allow_edit'] == true, ); } @@ -111,6 +117,7 @@ class AddressModel extends HiveObject { 'is_default': isDefault, if (cityName != null && cityName!.isNotEmpty) 'city_name': cityName, if (wardName != null && wardName!.isNotEmpty) 'ward_name': wardName, + 'is_allow_edit': isAllowEdit, }; } @@ -129,6 +136,7 @@ class AddressModel extends HiveObject { isDefault: isDefault, cityName: cityName, wardName: wardName, + isAllowEdit: isAllowEdit, ); } @@ -147,12 +155,14 @@ class AddressModel extends HiveObject { isDefault: entity.isDefault, cityName: entity.cityName, wardName: entity.wardName, + isAllowEdit: entity.isAllowEdit, ); } @override String toString() { return 'AddressModel(name: $name, addressTitle: $addressTitle, ' - 'addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault)'; + 'addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault, ' + 'isAllowEdit: $isAllowEdit)'; } } diff --git a/lib/features/account/data/models/address_model.g.dart b/lib/features/account/data/models/address_model.g.dart index 7658ee8..ca0e064 100644 --- a/lib/features/account/data/models/address_model.g.dart +++ b/lib/features/account/data/models/address_model.g.dart @@ -29,13 +29,14 @@ class AddressModelAdapter extends TypeAdapter { isDefault: fields[9] == null ? false : fields[9] as bool, cityName: fields[10] as String?, wardName: fields[11] as String?, + isAllowEdit: fields[12] == null ? true : fields[12] as bool, ); } @override void write(BinaryWriter writer, AddressModel obj) { writer - ..writeByte(12) + ..writeByte(13) ..writeByte(0) ..write(obj.name) ..writeByte(1) @@ -59,7 +60,9 @@ class AddressModelAdapter extends TypeAdapter { ..writeByte(10) ..write(obj.cityName) ..writeByte(11) - ..write(obj.wardName); + ..write(obj.wardName) + ..writeByte(12) + ..write(obj.isAllowEdit); } @override diff --git a/lib/features/account/domain/entities/address.dart b/lib/features/account/domain/entities/address.dart index b96ecfa..5f1567a 100644 --- a/lib/features/account/domain/entities/address.dart +++ b/lib/features/account/domain/entities/address.dart @@ -22,6 +22,7 @@ class Address extends Equatable { final bool isDefault; final String? cityName; final String? wardName; + final bool isAllowEdit; const Address({ required this.name, @@ -36,6 +37,7 @@ class Address extends Equatable { this.isDefault = false, this.cityName, this.wardName, + this.isAllowEdit = true, }); @override @@ -52,6 +54,7 @@ class Address extends Equatable { isDefault, cityName, wardName, + isAllowEdit, ]; /// Get full address display string @@ -81,6 +84,7 @@ class Address extends Equatable { bool? isDefault, String? cityName, String? wardName, + bool? isAllowEdit, }) { return Address( name: name ?? this.name, @@ -95,11 +99,12 @@ class Address extends Equatable { isDefault: isDefault ?? this.isDefault, cityName: cityName ?? this.cityName, wardName: wardName ?? this.wardName, + isAllowEdit: isAllowEdit ?? this.isAllowEdit, ); } @override String toString() { - return 'Address(name: $name, addressTitle: $addressTitle, addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault)'; + return 'Address(name: $name, addressTitle: $addressTitle, addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault, isAllowEdit: $isAllowEdit)'; } } diff --git a/lib/features/orders/data/datasources/order_status_local_datasource.dart b/lib/features/orders/data/datasources/order_status_local_datasource.dart index 18063c4..776f1e6 100644 --- a/lib/features/orders/data/datasources/order_status_local_datasource.dart +++ b/lib/features/orders/data/datasources/order_status_local_datasource.dart @@ -26,9 +26,9 @@ class OrderStatusLocalDataSource { /// Get cached order status list List getCachedStatusList() { try { - final values = _box.values.whereType().toList(); + final values = _box.values.whereType().toList() // Sort by index - values.sort((a, b) => a.index.compareTo(b.index)); + ..sort((a, b) => a.index.compareTo(b.index)); return values; } catch (e) { return []; diff --git a/lib/features/orders/presentation/pages/order_detail_page.dart b/lib/features/orders/presentation/pages/order_detail_page.dart index 5cf1737..d611feb 100644 --- a/lib/features/orders/presentation/pages/order_detail_page.dart +++ b/lib/features/orders/presentation/pages/order_detail_page.dart @@ -111,8 +111,9 @@ class OrderDetailPage extends ConsumerWidget { _buildPaymentHistoryCard(context, orderDetail), ], - // Cancel Order Button (only show for "Chờ phê duyệt" status) - if (orderDetail.order.status == 'Chờ phê duyệt') ...[ + // Cancel Order Button + // Use API flag to determine if cancellation is allowed + if (orderDetail.order.isAllowCancel) ...[ const SizedBox(height: 16), _buildCancelOrderButton(context, ref, orderDetail), ], @@ -405,90 +406,98 @@ class OrderDetailPage extends ConsumerWidget { color: AppColors.grey500, ), ), - TextButton( - onPressed: () async { - // Navigate to address selection and wait for result - final result = await context.push
( - '/account/addresses', - extra: { - 'selectMode': true, - 'currentAddress': shippingAddress, - }, - ); - - // If user selected an address, update the order - if (result != null && context.mounted) { - // Show loading indicator - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Đang cập nhật địa chỉ...'), - duration: Duration(seconds: 1), - ), - ); - - // Update shipping address (keep billing address the same) - await ref - .read(updateOrderAddressProvider.notifier) - .updateAddress( - orderId: orderId, - shippingAddressName: result.name, - customerAddress: orderDetail.billingAddress.name, + Builder( + builder: (context) { + // Use API flag to determine if editing is allowed + if (orderDetail.shippingAddress.isAllowEdit) { + return TextButton( + onPressed: () async { + // Navigate to address selection and wait for result + final result = await context.push
( + '/account/addresses', + extra: { + 'selectMode': true, + 'currentAddress': shippingAddress, + }, ); - // Check if update was successful - final updateState = - ref.read(updateOrderAddressProvider) - ..when( - data: (_) { - // Refresh order detail to show updated address - ref.invalidate(orderDetailProvider(orderId)); + // If user selected an address, update the order + if (result != null && context.mounted) { + // Show loading indicator + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Đang cập nhật địa chỉ...'), + duration: Duration(seconds: 1), + ), + ); - // Show success message - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Cập nhật địa chỉ giao hàng thành công', - ), - backgroundColor: AppColors.success, - ), + // Update shipping address (keep billing address the same) + await ref + .read(updateOrderAddressProvider.notifier) + .updateAddress( + orderId: orderId, + shippingAddressName: result.name, + customerAddress: orderDetail.billingAddress.name, + ); + + // Check if update was successful + final updateState = + ref.read(updateOrderAddressProvider) + ..when( + data: (_) { + // Refresh order detail to show updated address + ref.invalidate(orderDetailProvider(orderId)); + + // Show success message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Cập nhật địa chỉ giao hàng thành công', + ), + backgroundColor: AppColors.success, + ), + ); + } + }, + error: (error, _) { + // Show error message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Lỗi: ${error.toString()}', + ), + backgroundColor: AppColors.danger, + ), + ); + } + }, + loading: () {}, ); - } - }, - error: (error, _) { - // Show error message - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Lỗi: ${error.toString()}', - ), - backgroundColor: AppColors.danger, - ), - ); - } - }, - loading: () {}, - ); - } + } + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: const BorderSide(color: AppColors.grey100), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Cập nhật', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryBlue, + ), + ), + ); + } + return const SizedBox.shrink(); }, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - side: const BorderSide(color: AppColors.grey100), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: const Text( - 'Cập nhật', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: AppColors.primaryBlue, - ), - ), ), ], ), @@ -621,91 +630,100 @@ class OrderDetailPage extends ConsumerWidget { ), ], ), - TextButton( - onPressed: () async { - // Navigate to address selection and wait for result - final result = await context.push
( - '/account/addresses', - extra: { - 'selectMode': true, - 'currentAddress': billingAddress, - }, - ); - - // If user selected an address, update the order - if (result != null && context.mounted) { - // Show loading indicator - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Đang cập nhật địa chỉ...'), - duration: Duration(seconds: 1), - ), - ); - - // Update billing address (keep shipping address the same) - await ref - .read(updateOrderAddressProvider.notifier) - .updateAddress( - orderId: orderId, - shippingAddressName: orderDetail.shippingAddress.name, - customerAddress: result.name, + Builder( + builder: (context) { + // Use API flag to determine if editing is allowed + if (orderDetail.billingAddress.isAllowEdit) { + return TextButton( + onPressed: () async { + // Navigate to address selection and wait for result + final result = await context.push
( + '/account/addresses', + extra: { + 'selectMode': true, + 'currentAddress': billingAddress, + }, ); - // Check if update was successful - final updateState = - ref.read(updateOrderAddressProvider) - ..when( - data: (_) { - // Refresh order detail to show updated address - ref.invalidate(orderDetailProvider(orderId)); + // If user selected an address, update the order + if (result != null && context.mounted) { + // Show loading indicator + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Đang cập nhật địa chỉ...'), + duration: Duration(seconds: 1), + ), + ); - // Show success message - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Cập nhật địa chỉ hóa đơn thành công', - ), - backgroundColor: AppColors.success, - ), + // Update billing address (keep shipping address the same) + await ref + .read(updateOrderAddressProvider.notifier) + .updateAddress( + orderId: orderId, + shippingAddressName: orderDetail.shippingAddress.name, + customerAddress: result.name, + ); + + // Check if update was successful + final updateState = + ref.read(updateOrderAddressProvider) + ..when( + data: (_) { + // Refresh order detail to show updated address + ref.invalidate(orderDetailProvider(orderId)); + + // Show success message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Cập nhật địa chỉ hóa đơn thành công', + ), + backgroundColor: AppColors.success, + ), + ); + } + }, + error: (error, _) { + // Show error message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Lỗi: ${error.toString()}', + ), + backgroundColor: AppColors.danger, + ), + ); + } + }, + loading: () {}, ); - } - }, - error: (error, _) { - // Show error message - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Lỗi: ${error.toString()}', - ), - backgroundColor: AppColors.danger, - ), - ); - } - }, - loading: () {}, - ); - } + } + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: const BorderSide(color: AppColors.grey100), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Cập nhật', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.primaryBlue, + ), + ), + ); + } + return const SizedBox.shrink(); }, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - side: const BorderSide(color: AppColors.grey100), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: const Text( - 'Cập nhật', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: AppColors.primaryBlue, - ), - ), ), + ], ), diff --git a/lib/features/orders/presentation/providers/orders_provider.dart b/lib/features/orders/presentation/providers/orders_provider.dart index 88b50e7..e989ae3 100644 --- a/lib/features/orders/presentation/providers/orders_provider.dart +++ b/lib/features/orders/presentation/providers/orders_provider.dart @@ -3,7 +3,11 @@ /// Riverpod providers for managing orders state. library; +import 'package:hive_ce/hive.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:worker/core/constants/storage_constants.dart'; +import 'package:worker/features/orders/data/datasources/order_status_local_datasource.dart'; +import 'package:worker/features/orders/data/models/order_status_model.dart'; import 'package:worker/features/orders/domain/entities/order.dart'; import 'package:worker/features/orders/domain/entities/order_detail.dart'; import 'package:worker/features/orders/domain/entities/order_status.dart'; @@ -171,6 +175,37 @@ Future totalOrdersCount(Ref ref) async { ); } +/// Order Status Hive Box Provider +/// +/// Provides direct access to the Hive box for order statuses. +/// Use this to read data directly from Hive in the UI. +@riverpod +Box orderStatusHiveBox(Ref ref) { + return Hive.box(HiveBoxNames.orderStatusBox); +} + +/// Helper: Get Order Status by Label +/// +/// Returns the OrderStatusModel for a given label (e.g., "Chờ phê duyệt"). +/// Returns null if not found. +OrderStatusModel? getOrderStatusByLabel(Box box, String label) { + final statuses = box.values.whereType(); + try { + return statuses.firstWhere((status) => status.label == label); + } catch (e) { + return null; + } +} + +/// Helper: Get Order Status by Index +/// +/// Returns the OrderStatusModel for a given index. +/// Returns null if not found. +OrderStatusModel? getOrderStatusByIndex(Box box, int index) { + final status = box.get(index); + return status is OrderStatusModel ? status : null; +} + /// Order Status List Provider /// /// Provides cached order status list with automatic refresh. diff --git a/lib/features/orders/presentation/providers/orders_provider.g.dart b/lib/features/orders/presentation/providers/orders_provider.g.dart index 4cd218a..be10f16 100644 --- a/lib/features/orders/presentation/providers/orders_provider.g.dart +++ b/lib/features/orders/presentation/providers/orders_provider.g.dart @@ -344,6 +344,62 @@ final class TotalOrdersCountProvider String _$totalOrdersCountHash() => r'ec1ab3a8d432033aa1f02d28e841e78eba06d63e'; +/// Order Status Hive Box Provider +/// +/// Provides direct access to the Hive box for order statuses. +/// Use this to read data directly from Hive in the UI. + +@ProviderFor(orderStatusHiveBox) +const orderStatusHiveBoxProvider = OrderStatusHiveBoxProvider._(); + +/// Order Status Hive Box Provider +/// +/// Provides direct access to the Hive box for order statuses. +/// Use this to read data directly from Hive in the UI. + +final class OrderStatusHiveBoxProvider + extends $FunctionalProvider, Box, Box> + with $Provider> { + /// Order Status Hive Box Provider + /// + /// Provides direct access to the Hive box for order statuses. + /// Use this to read data directly from Hive in the UI. + const OrderStatusHiveBoxProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'orderStatusHiveBoxProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$orderStatusHiveBoxHash(); + + @$internal + @override + $ProviderElement> $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + Box create(Ref ref) { + return orderStatusHiveBox(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Box value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + +String _$orderStatusHiveBoxHash() => + r'49b681c9cabb60f58e049dd4d89872c827d7db9a'; + /// Order Status List Provider /// /// Provides cached order status list with automatic refresh. diff --git a/pubspec.yaml b/pubspec.yaml index 479f83e..3dfa6d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.1+12 +version: 1.0.1+15 environment: sdk: ^3.10.0