This commit is contained in:
Phuoc Nguyen
2025-11-25 18:00:01 +07:00
parent 84669ac89c
commit 5e9b0cb562
9 changed files with 337 additions and 167 deletions

View File

@@ -197,7 +197,50 @@ class HiveKeys {
static const String lastSyncTime = 'last_sync_time'; static const String lastSyncTime = 'last_sync_time';
static const String schemaVersion = 'schema_version'; static const String schemaVersion = 'schema_version';
static const String encryptionEnabled = 'encryption_enabled'; 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 // Cache Box Keys
static const String productsCacheKey = 'products_cache'; static const String productsCacheKey = 'products_cache';
static const String categoriesCacheKey = 'categories_cache'; static const String categoriesCacheKey = 'categories_cache';

View File

@@ -62,6 +62,10 @@ class AddressModel extends HiveObject {
@HiveField(11) @HiveField(11)
String? wardName; String? wardName;
/// Whether editing this address is allowed
@HiveField(12)
bool isAllowEdit;
AddressModel({ AddressModel({
required this.name, required this.name,
required this.addressTitle, required this.addressTitle,
@@ -75,6 +79,7 @@ class AddressModel extends HiveObject {
this.isDefault = false, this.isDefault = false,
this.cityName, this.cityName,
this.wardName, this.wardName,
this.isAllowEdit = true,
}); });
/// Create from JSON (API response) /// Create from JSON (API response)
@@ -92,6 +97,7 @@ class AddressModel extends HiveObject {
isDefault: json['is_default'] == 1 || json['is_default'] == true, isDefault: json['is_default'] == 1 || json['is_default'] == true,
cityName: json['city_name'] as String?, cityName: json['city_name'] as String?,
wardName: json['ward_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, 'is_default': isDefault,
if (cityName != null && cityName!.isNotEmpty) 'city_name': cityName, if (cityName != null && cityName!.isNotEmpty) 'city_name': cityName,
if (wardName != null && wardName!.isNotEmpty) 'ward_name': wardName, if (wardName != null && wardName!.isNotEmpty) 'ward_name': wardName,
'is_allow_edit': isAllowEdit,
}; };
} }
@@ -129,6 +136,7 @@ class AddressModel extends HiveObject {
isDefault: isDefault, isDefault: isDefault,
cityName: cityName, cityName: cityName,
wardName: wardName, wardName: wardName,
isAllowEdit: isAllowEdit,
); );
} }
@@ -147,12 +155,14 @@ class AddressModel extends HiveObject {
isDefault: entity.isDefault, isDefault: entity.isDefault,
cityName: entity.cityName, cityName: entity.cityName,
wardName: entity.wardName, wardName: entity.wardName,
isAllowEdit: entity.isAllowEdit,
); );
} }
@override @override
String toString() { String toString() {
return 'AddressModel(name: $name, addressTitle: $addressTitle, ' return 'AddressModel(name: $name, addressTitle: $addressTitle, '
'addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault)'; 'addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault, '
'isAllowEdit: $isAllowEdit)';
} }
} }

View File

@@ -29,13 +29,14 @@ class AddressModelAdapter extends TypeAdapter<AddressModel> {
isDefault: fields[9] == null ? false : fields[9] as bool, isDefault: fields[9] == null ? false : fields[9] as bool,
cityName: fields[10] as String?, cityName: fields[10] as String?,
wardName: fields[11] as String?, wardName: fields[11] as String?,
isAllowEdit: fields[12] == null ? true : fields[12] as bool,
); );
} }
@override @override
void write(BinaryWriter writer, AddressModel obj) { void write(BinaryWriter writer, AddressModel obj) {
writer writer
..writeByte(12) ..writeByte(13)
..writeByte(0) ..writeByte(0)
..write(obj.name) ..write(obj.name)
..writeByte(1) ..writeByte(1)
@@ -59,7 +60,9 @@ class AddressModelAdapter extends TypeAdapter<AddressModel> {
..writeByte(10) ..writeByte(10)
..write(obj.cityName) ..write(obj.cityName)
..writeByte(11) ..writeByte(11)
..write(obj.wardName); ..write(obj.wardName)
..writeByte(12)
..write(obj.isAllowEdit);
} }
@override @override

View File

@@ -22,6 +22,7 @@ class Address extends Equatable {
final bool isDefault; final bool isDefault;
final String? cityName; final String? cityName;
final String? wardName; final String? wardName;
final bool isAllowEdit;
const Address({ const Address({
required this.name, required this.name,
@@ -36,6 +37,7 @@ class Address extends Equatable {
this.isDefault = false, this.isDefault = false,
this.cityName, this.cityName,
this.wardName, this.wardName,
this.isAllowEdit = true,
}); });
@override @override
@@ -52,6 +54,7 @@ class Address extends Equatable {
isDefault, isDefault,
cityName, cityName,
wardName, wardName,
isAllowEdit,
]; ];
/// Get full address display string /// Get full address display string
@@ -81,6 +84,7 @@ class Address extends Equatable {
bool? isDefault, bool? isDefault,
String? cityName, String? cityName,
String? wardName, String? wardName,
bool? isAllowEdit,
}) { }) {
return Address( return Address(
name: name ?? this.name, name: name ?? this.name,
@@ -95,11 +99,12 @@ class Address extends Equatable {
isDefault: isDefault ?? this.isDefault, isDefault: isDefault ?? this.isDefault,
cityName: cityName ?? this.cityName, cityName: cityName ?? this.cityName,
wardName: wardName ?? this.wardName, wardName: wardName ?? this.wardName,
isAllowEdit: isAllowEdit ?? this.isAllowEdit,
); );
} }
@override @override
String toString() { 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)';
} }
} }

View File

@@ -26,9 +26,9 @@ class OrderStatusLocalDataSource {
/// Get cached order status list /// Get cached order status list
List<OrderStatusModel> getCachedStatusList() { List<OrderStatusModel> getCachedStatusList() {
try { try {
final values = _box.values.whereType<OrderStatusModel>().toList(); final values = _box.values.whereType<OrderStatusModel>().toList()
// Sort by index // Sort by index
values.sort((a, b) => a.index.compareTo(b.index)); ..sort((a, b) => a.index.compareTo(b.index));
return values; return values;
} catch (e) { } catch (e) {
return []; return [];

View File

@@ -111,8 +111,9 @@ class OrderDetailPage extends ConsumerWidget {
_buildPaymentHistoryCard(context, orderDetail), _buildPaymentHistoryCard(context, orderDetail),
], ],
// Cancel Order Button (only show for "Chờ phê duyệt" status) // Cancel Order Button
if (orderDetail.order.status == 'Chờ phê duyệt') ...[ // Use API flag to determine if cancellation is allowed
if (orderDetail.order.isAllowCancel) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
_buildCancelOrderButton(context, ref, orderDetail), _buildCancelOrderButton(context, ref, orderDetail),
], ],
@@ -405,90 +406,98 @@ class OrderDetailPage extends ConsumerWidget {
color: AppColors.grey500, color: AppColors.grey500,
), ),
), ),
TextButton( Builder(
onPressed: () async { builder: (context) {
// Navigate to address selection and wait for result // Use API flag to determine if editing is allowed
final result = await context.push<Address>( if (orderDetail.shippingAddress.isAllowEdit) {
'/account/addresses', return TextButton(
extra: { onPressed: () async {
'selectMode': true, // Navigate to address selection and wait for result
'currentAddress': shippingAddress, final result = await context.push<Address>(
}, '/account/addresses',
); extra: {
'selectMode': true,
// If user selected an address, update the order 'currentAddress': shippingAddress,
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,
); );
// Check if update was successful // If user selected an address, update the order
final updateState = if (result != null && context.mounted) {
ref.read(updateOrderAddressProvider) // Show loading indicator
..when( ScaffoldMessenger.of(context).showSnackBar(
data: (_) { const SnackBar(
// Refresh order detail to show updated address content: Text('Đang cập nhật địa chỉ...'),
ref.invalidate(orderDetailProvider(orderId)); duration: Duration(seconds: 1),
),
);
// Show success message // Update shipping address (keep billing address the same)
if (context.mounted) { await ref
ScaffoldMessenger.of(context).showSnackBar( .read(updateOrderAddressProvider.notifier)
const SnackBar( .updateAddress(
content: Text( orderId: orderId,
'Cập nhật địa chỉ giao hàng thành công', shippingAddressName: result.name,
), customerAddress: orderDetail.billingAddress.name,
backgroundColor: AppColors.success, );
),
// 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, _) { style: TextButton.styleFrom(
// Show error message padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
if (context.mounted) { minimumSize: Size.zero,
ScaffoldMessenger.of(context).showSnackBar( tapTargetSize: MaterialTapTargetSize.shrinkWrap,
SnackBar( side: const BorderSide(color: AppColors.grey100),
content: Text( shape: RoundedRectangleBorder(
'Lỗi: ${error.toString()}', borderRadius: BorderRadius.circular(8),
), ),
backgroundColor: AppColors.danger, ),
), child: const Text(
); 'Cập nhật',
} style: TextStyle(
}, fontSize: 14,
loading: () {}, 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( Builder(
onPressed: () async { builder: (context) {
// Navigate to address selection and wait for result // Use API flag to determine if editing is allowed
final result = await context.push<Address>( if (orderDetail.billingAddress.isAllowEdit) {
'/account/addresses', return TextButton(
extra: { onPressed: () async {
'selectMode': true, // Navigate to address selection and wait for result
'currentAddress': billingAddress, final result = await context.push<Address>(
}, '/account/addresses',
); extra: {
'selectMode': true,
// If user selected an address, update the order 'currentAddress': billingAddress,
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,
); );
// Check if update was successful // If user selected an address, update the order
final updateState = if (result != null && context.mounted) {
ref.read(updateOrderAddressProvider) // Show loading indicator
..when( ScaffoldMessenger.of(context).showSnackBar(
data: (_) { const SnackBar(
// Refresh order detail to show updated address content: Text('Đang cập nhật địa chỉ...'),
ref.invalidate(orderDetailProvider(orderId)); duration: Duration(seconds: 1),
),
);
// Show success message // Update billing address (keep shipping address the same)
if (context.mounted) { await ref
ScaffoldMessenger.of(context).showSnackBar( .read(updateOrderAddressProvider.notifier)
const SnackBar( .updateAddress(
content: Text( orderId: orderId,
'Cập nhật địa chỉ hóa đơn thành công', shippingAddressName: orderDetail.shippingAddress.name,
), customerAddress: result.name,
backgroundColor: AppColors.success, );
),
// 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, _) { style: TextButton.styleFrom(
// Show error message padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
if (context.mounted) { minimumSize: Size.zero,
ScaffoldMessenger.of(context).showSnackBar( tapTargetSize: MaterialTapTargetSize.shrinkWrap,
SnackBar( side: const BorderSide(color: AppColors.grey100),
content: Text( shape: RoundedRectangleBorder(
'Lỗi: ${error.toString()}', borderRadius: BorderRadius.circular(8),
), ),
backgroundColor: AppColors.danger, ),
), child: const Text(
); 'Cập nhật',
} style: TextStyle(
}, fontSize: 14,
loading: () {}, 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,
),
),
), ),
], ],
), ),

View File

@@ -3,7 +3,11 @@
/// Riverpod providers for managing orders state. /// Riverpod providers for managing orders state.
library; library;
import 'package:hive_ce/hive.dart';
import 'package:riverpod_annotation/riverpod_annotation.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.dart';
import 'package:worker/features/orders/domain/entities/order_detail.dart'; import 'package:worker/features/orders/domain/entities/order_detail.dart';
import 'package:worker/features/orders/domain/entities/order_status.dart'; import 'package:worker/features/orders/domain/entities/order_status.dart';
@@ -171,6 +175,37 @@ Future<int> 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<dynamic> 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<dynamic> box, String label) {
final statuses = box.values.whereType<OrderStatusModel>();
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<dynamic> box, int index) {
final status = box.get(index);
return status is OrderStatusModel ? status : null;
}
/// Order Status List Provider /// Order Status List Provider
/// ///
/// Provides cached order status list with automatic refresh. /// Provides cached order status list with automatic refresh.

View File

@@ -344,6 +344,62 @@ final class TotalOrdersCountProvider
String _$totalOrdersCountHash() => r'ec1ab3a8d432033aa1f02d28e841e78eba06d63e'; 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<dynamic>, Box<dynamic>, Box<dynamic>>
with $Provider<Box<dynamic>> {
/// 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<Box<dynamic>> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
Box<dynamic> create(Ref ref) {
return orderStatusHiveBox(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Box<dynamic> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Box<dynamic>>(value),
);
}
}
String _$orderStatusHiveBoxHash() =>
r'49b681c9cabb60f58e049dd4d89872c827d7db9a';
/// Order Status List Provider /// Order Status List Provider
/// ///
/// Provides cached order status list with automatic refresh. /// Provides cached order status list with automatic refresh.

View File

@@ -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 # 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 # 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. # 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: environment:
sdk: ^3.10.0 sdk: ^3.10.0