Compare commits
3 Commits
cae04b3ae7
...
2dadcc5ce1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dadcc5ce1 | ||
|
|
27798cc234 | ||
|
|
e1c9f818d2 |
@@ -1,4 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="worker"
|
android:label="worker"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
|
|||||||
{
|
{
|
||||||
"item_id": "Bình giữ nhiệt Euroutile",
|
"item_id": "Bình giữ nhiệt Euroutile",
|
||||||
"amount": 3000000,
|
"amount": 3000000,
|
||||||
"quantity" : 5.78
|
"quantity" : 5.78,
|
||||||
|
"conversion_of_sm: 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"item_id": "Gạch ốp Signature SIG.P-8806",
|
"item_id": "Gạch ốp Signature SIG.P-8806",
|
||||||
"amount": 4000000,
|
"amount": 4000000,
|
||||||
"quantity" : 33
|
"quantity" : 33,
|
||||||
|
"conversion_of_sm: 1.5
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}'
|
}'
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
|||||||
--data '{
|
--data '{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"fields": ["item_group_name","name"],
|
"fields": ["item_group_name","name"],
|
||||||
"filters": {"is_group": 0},
|
"filters": {"is_group": 0, "custom_published" : 1},
|
||||||
"limit_page_length": 0
|
"limit_page_length": 0
|
||||||
}'
|
}'
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
<string>Ứng dụng cần quyền truy cập microphone để ghi âm video nếu cần</string>
|
<string>Ứng dụng cần quyền truy cập microphone để ghi âm video nếu cần</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Ứng dụng cần quyền truy cập thư viện ảnh để chọn ảnh giấy tờ xác thực và mã QR từ thiết bị của bạn</string>
|
<string>Ứng dụng cần quyền truy cập thư viện ảnh để chọn ảnh giấy tờ xác thực và mã QR từ thiết bị của bạn</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>Ứng dụng sử dụng vị trí để cải thiện trải nghiệm và đề xuất showroom gần bạn</string>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
|
|||||||
@@ -249,13 +249,13 @@ class CustomCurlLoggerInterceptor extends Interceptor {
|
|||||||
@override
|
@override
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
final curl = _cURLRepresentation(options);
|
final curl = _cURLRepresentation(options);
|
||||||
debugPrint(
|
// debugPrint(
|
||||||
'╔╣ CURL Request ╠══════════════════════════════════════════════════',
|
// '╔╣ CURL Request ╠══════════════════════════════════════════════════',
|
||||||
);
|
// );
|
||||||
debugPrint(curl);
|
// debugPrint(curl);
|
||||||
debugPrint(
|
// debugPrint(
|
||||||
'╚═════════════════════════════════════════════════════════════════',
|
// '╚═════════════════════════════════════════════════════════════════',
|
||||||
);
|
// );
|
||||||
// Also log to dart:developer for better filtering in DevTools
|
// Also log to dart:developer for better filtering in DevTools
|
||||||
developer.log(curl, name: 'DIO_CURL', time: DateTime.now());
|
developer.log(curl, name: 'DIO_CURL', time: DateTime.now());
|
||||||
handler.next(options);
|
handler.next(options);
|
||||||
@@ -468,7 +468,7 @@ Future<Dio> dio(Ref ref) async {
|
|||||||
// Add interceptors in order
|
// Add interceptors in order
|
||||||
// 1. Custom Curl interceptor (first to log cURL commands)
|
// 1. Custom Curl interceptor (first to log cURL commands)
|
||||||
// Uses debugPrint and developer.log for better visibility
|
// Uses debugPrint and developer.log for better visibility
|
||||||
// ..interceptors.add(CustomCurlLoggerInterceptor())
|
..interceptors.add(CustomCurlLoggerInterceptor())
|
||||||
// 2. Logging interceptor
|
// 2. Logging interceptor
|
||||||
..interceptors.add(ref.watch(loggingInterceptorProvider))
|
..interceptors.add(ref.watch(loggingInterceptorProvider))
|
||||||
// 3. Auth interceptor (add tokens to requests)
|
// 3. Auth interceptor (add tokens to requests)
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ final class DioProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$dioHash() => r'f15495e99d11744c245e2be892657748aeeb8ae7';
|
String _$dioHash() => r'd15bfe824d6501e5cbd56ff152de978030d97be4';
|
||||||
|
|
||||||
/// Provider for DioClient
|
/// Provider for DioClient
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ String _$connectivityHash() => r'6d67af0ea4110f6ee0246dd332f90f8901380eda';
|
|||||||
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
||||||
/// connectivityState.when(
|
/// connectivityState.when(
|
||||||
/// data: (status) => Text('Status: $status'),
|
/// data: (status) => Text('Status: $status'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -81,7 +81,7 @@ const connectivityStreamProvider = ConnectivityStreamProvider._();
|
|||||||
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
||||||
/// connectivityState.when(
|
/// connectivityState.when(
|
||||||
/// data: (status) => Text('Status: $status'),
|
/// data: (status) => Text('Status: $status'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -104,7 +104,7 @@ final class ConnectivityStreamProvider
|
|||||||
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
||||||
/// connectivityState.when(
|
/// connectivityState.when(
|
||||||
/// data: (status) => Text('Status: $status'),
|
/// data: (status) => Text('Status: $status'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -219,7 +219,7 @@ String _$currentConnectivityHash() =>
|
|||||||
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
||||||
/// isOnlineAsync.when(
|
/// isOnlineAsync.when(
|
||||||
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -235,7 +235,7 @@ const isOnlineProvider = IsOnlineProvider._();
|
|||||||
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
||||||
/// isOnlineAsync.when(
|
/// isOnlineAsync.when(
|
||||||
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -251,7 +251,7 @@ final class IsOnlineProvider
|
|||||||
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
||||||
/// isOnlineAsync.when(
|
/// isOnlineAsync.when(
|
||||||
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -52,13 +52,39 @@ class AnalyticsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// E-commerce Events
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Log view item event - when user views product detail
|
||||||
|
static Future<void> logViewItem({
|
||||||
|
required String productId,
|
||||||
|
required String productName,
|
||||||
|
required double price,
|
||||||
|
String? brand,
|
||||||
|
String? category,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logViewItem(
|
||||||
|
currency: 'VND',
|
||||||
|
value: price,
|
||||||
|
items: [
|
||||||
|
AnalyticsEventItem(
|
||||||
|
itemId: productId,
|
||||||
|
itemName: productName,
|
||||||
|
price: price,
|
||||||
|
itemBrand: brand,
|
||||||
|
itemCategory: category,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
debugPrint('📊 Analytics: view_item - $productName');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Log add to cart event
|
/// Log add to cart event
|
||||||
///
|
|
||||||
/// [productId] - Product SKU or ID
|
|
||||||
/// [productName] - Product display name
|
|
||||||
/// [price] - Unit price in VND
|
|
||||||
/// [quantity] - Quantity added
|
|
||||||
/// [category] - Optional product category
|
|
||||||
static Future<void> logAddToCart({
|
static Future<void> logAddToCart({
|
||||||
required String productId,
|
required String productId,
|
||||||
required String productName,
|
required String productName,
|
||||||
@@ -85,4 +111,252 @@ class AnalyticsService {
|
|||||||
debugPrint('📊 Analytics error: $e');
|
debugPrint('📊 Analytics error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Log remove from cart event
|
||||||
|
static Future<void> logRemoveFromCart({
|
||||||
|
required String productId,
|
||||||
|
required String productName,
|
||||||
|
required double price,
|
||||||
|
required int quantity,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logRemoveFromCart(
|
||||||
|
currency: 'VND',
|
||||||
|
value: price * quantity,
|
||||||
|
items: [
|
||||||
|
AnalyticsEventItem(
|
||||||
|
itemId: productId,
|
||||||
|
itemName: productName,
|
||||||
|
price: price,
|
||||||
|
quantity: quantity,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
debugPrint('📊 Analytics: remove_from_cart - $productName');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log view cart event
|
||||||
|
static Future<void> logViewCart({
|
||||||
|
required double cartValue,
|
||||||
|
required List<AnalyticsEventItem> items,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logViewCart(
|
||||||
|
currency: 'VND',
|
||||||
|
value: cartValue,
|
||||||
|
items: items,
|
||||||
|
);
|
||||||
|
debugPrint('📊 Analytics: view_cart - ${items.length} items');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log begin checkout event
|
||||||
|
static Future<void> logBeginCheckout({
|
||||||
|
required double value,
|
||||||
|
required List<AnalyticsEventItem> items,
|
||||||
|
String? coupon,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logBeginCheckout(
|
||||||
|
currency: 'VND',
|
||||||
|
value: value,
|
||||||
|
items: items,
|
||||||
|
coupon: coupon,
|
||||||
|
);
|
||||||
|
debugPrint('📊 Analytics: begin_checkout - $value VND');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log purchase event - when order is completed
|
||||||
|
static Future<void> logPurchase({
|
||||||
|
required String orderId,
|
||||||
|
required double value,
|
||||||
|
required List<AnalyticsEventItem> items,
|
||||||
|
double? shipping,
|
||||||
|
double? tax,
|
||||||
|
String? coupon,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logPurchase(
|
||||||
|
currency: 'VND',
|
||||||
|
transactionId: orderId,
|
||||||
|
value: value,
|
||||||
|
items: items,
|
||||||
|
shipping: shipping,
|
||||||
|
tax: tax,
|
||||||
|
coupon: coupon,
|
||||||
|
);
|
||||||
|
debugPrint('📊 Analytics: purchase - Order $orderId');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Search & Discovery Events
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Log search event
|
||||||
|
static Future<void> logSearch({
|
||||||
|
required String searchTerm,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logSearch(searchTerm: searchTerm);
|
||||||
|
debugPrint('📊 Analytics: search - $searchTerm');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log select item event - when user taps on a product in list
|
||||||
|
static Future<void> logSelectItem({
|
||||||
|
required String productId,
|
||||||
|
required String productName,
|
||||||
|
String? listName,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logSelectItem(
|
||||||
|
itemListName: listName,
|
||||||
|
items: [
|
||||||
|
AnalyticsEventItem(
|
||||||
|
itemId: productId,
|
||||||
|
itemName: productName,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
debugPrint('📊 Analytics: select_item - $productName');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Loyalty & Rewards Events
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Log earn points event
|
||||||
|
static Future<void> logEarnPoints({
|
||||||
|
required int points,
|
||||||
|
required String source,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logEarnVirtualCurrency(
|
||||||
|
virtualCurrencyName: 'loyalty_points',
|
||||||
|
value: points.toDouble(),
|
||||||
|
);
|
||||||
|
debugPrint('📊 Analytics: earn_points - $points from $source');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log spend points event - when user redeems points
|
||||||
|
static Future<void> logSpendPoints({
|
||||||
|
required int points,
|
||||||
|
required String itemName,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logSpendVirtualCurrency(
|
||||||
|
virtualCurrencyName: 'loyalty_points',
|
||||||
|
value: points.toDouble(),
|
||||||
|
itemName: itemName,
|
||||||
|
);
|
||||||
|
debugPrint('📊 Analytics: spend_points - $points for $itemName');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// User Events
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Log login event
|
||||||
|
static Future<void> logLogin({
|
||||||
|
String? method,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logLogin(loginMethod: method ?? 'phone');
|
||||||
|
debugPrint('📊 Analytics: login - $method');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log sign up event
|
||||||
|
static Future<void> logSignUp({
|
||||||
|
String? method,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logSignUp(signUpMethod: method ?? 'phone');
|
||||||
|
debugPrint('📊 Analytics: sign_up - $method');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log share event
|
||||||
|
static Future<void> logShare({
|
||||||
|
required String contentType,
|
||||||
|
required String itemId,
|
||||||
|
String? method,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logShare(
|
||||||
|
contentType: contentType,
|
||||||
|
itemId: itemId,
|
||||||
|
method: method ?? 'unknown',
|
||||||
|
);
|
||||||
|
debugPrint('📊 Analytics: share - $contentType $itemId');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Custom Events
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Log custom event
|
||||||
|
static Future<void> logEvent({
|
||||||
|
required String name,
|
||||||
|
Map<String, Object>? parameters,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.logEvent(name: name, parameters: parameters);
|
||||||
|
debugPrint('📊 Analytics: $name');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set user ID for analytics
|
||||||
|
static Future<void> setUserId(String? userId) async {
|
||||||
|
try {
|
||||||
|
await _analytics.setUserId(id: userId);
|
||||||
|
debugPrint('📊 Analytics: setUserId - $userId');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set user property
|
||||||
|
static Future<void> setUserProperty({
|
||||||
|
required String name,
|
||||||
|
required String? value,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _analytics.setUserProperty(name: name, value: value);
|
||||||
|
debugPrint('📊 Analytics: setUserProperty - $name: $value');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('📊 Analytics error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ String _$getUserInfoUseCaseHash() =>
|
|||||||
///
|
///
|
||||||
/// userInfoAsync.when(
|
/// userInfoAsync.when(
|
||||||
/// data: (userInfo) => Text(userInfo.fullName),
|
/// data: (userInfo) => Text(userInfo.fullName),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
@@ -184,7 +184,7 @@ const userInfoProvider = UserInfoProvider._();
|
|||||||
///
|
///
|
||||||
/// userInfoAsync.when(
|
/// userInfoAsync.when(
|
||||||
/// data: (userInfo) => Text(userInfo.fullName),
|
/// data: (userInfo) => Text(userInfo.fullName),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
@@ -206,7 +206,7 @@ final class UserInfoProvider
|
|||||||
///
|
///
|
||||||
/// userInfoAsync.when(
|
/// userInfoAsync.when(
|
||||||
/// data: (userInfo) => Text(userInfo.fullName),
|
/// data: (userInfo) => Text(userInfo.fullName),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
@@ -247,7 +247,7 @@ String _$userInfoHash() => r'ed28fdf0213dfd616592b9735cd291f147867047';
|
|||||||
///
|
///
|
||||||
/// userInfoAsync.when(
|
/// userInfoAsync.when(
|
||||||
/// data: (userInfo) => Text(userInfo.fullName),
|
/// data: (userInfo) => Text(userInfo.fullName),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -190,15 +190,21 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource {
|
|||||||
try {
|
try {
|
||||||
// Map API response to CartItemModel
|
// Map API response to CartItemModel
|
||||||
// API fields: name, item, quantity, amount, item_code, item_name, image, conversion_of_sm
|
// API fields: name, item, quantity, amount, item_code, item_name, image, conversion_of_sm
|
||||||
|
final quantity = (item['quantity'] as num?)?.toDouble() ?? 0.0;
|
||||||
|
final unitPrice = (item['amount'] as num?)?.toDouble() ?? 0.0;
|
||||||
|
|
||||||
final cartItem = CartItemModel(
|
final cartItem = CartItemModel(
|
||||||
cartItemId: item['name'] as String? ?? '',
|
cartItemId: item['name'] as String? ?? '',
|
||||||
cartId: 'user_cart', // Fixed cart ID for user's cart
|
cartId: 'user_cart', // Fixed cart ID for user's cart
|
||||||
productId: item['item_code'] as String? ?? item['item'] as String? ?? '',
|
productId: item['item_code'] as String? ?? item['item'] as String? ?? '',
|
||||||
quantity: (item['quantity'] as num?)?.toDouble() ?? 0.0,
|
quantity: quantity,
|
||||||
unitPrice: (item['amount'] as num?)?.toDouble() ?? 0.0,
|
unitPrice: unitPrice,
|
||||||
subtotal: ((item['quantity'] as num?)?.toDouble() ?? 0.0) *
|
subtotal: quantity * unitPrice,
|
||||||
((item['amount'] as num?)?.toDouble() ?? 0.0),
|
|
||||||
addedAt: DateTime.now(), // API doesn't provide timestamp
|
addedAt: DateTime.now(), // API doesn't provide timestamp
|
||||||
|
// Product details from cart API - no need to fetch separately
|
||||||
|
itemName: item['item_name'] as String?,
|
||||||
|
image: item['image'] as String?,
|
||||||
|
conversionOfSm: (item['conversion_of_sm'] as num?)?.toDouble(),
|
||||||
);
|
);
|
||||||
|
|
||||||
cartItems.add(cartItem);
|
cartItems.add(cartItem);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'package:hive_ce/hive.dart';
|
import 'package:hive_ce/hive.dart';
|
||||||
import 'package:worker/core/constants/storage_constants.dart';
|
import 'package:worker/core/constants/storage_constants.dart';
|
||||||
|
import 'package:worker/features/cart/domain/entities/cart_item.dart';
|
||||||
|
|
||||||
part 'cart_item_model.g.dart';
|
part 'cart_item_model.g.dart';
|
||||||
|
|
||||||
/// Cart Item Model - Type ID: 5
|
/// Cart Item Model - Type ID: 5
|
||||||
|
///
|
||||||
|
/// Includes product details from cart API to avoid fetching each product.
|
||||||
@HiveType(typeId: HiveTypeIds.cartItemModel)
|
@HiveType(typeId: HiveTypeIds.cartItemModel)
|
||||||
class CartItemModel extends HiveObject {
|
class CartItemModel extends HiveObject {
|
||||||
CartItemModel({
|
CartItemModel({
|
||||||
@@ -14,6 +17,9 @@ class CartItemModel extends HiveObject {
|
|||||||
required this.unitPrice,
|
required this.unitPrice,
|
||||||
required this.subtotal,
|
required this.subtotal,
|
||||||
required this.addedAt,
|
required this.addedAt,
|
||||||
|
this.itemName,
|
||||||
|
this.image,
|
||||||
|
this.conversionOfSm,
|
||||||
});
|
});
|
||||||
|
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
@@ -37,6 +43,18 @@ class CartItemModel extends HiveObject {
|
|||||||
@HiveField(6)
|
@HiveField(6)
|
||||||
final DateTime addedAt;
|
final DateTime addedAt;
|
||||||
|
|
||||||
|
/// Product name from cart API
|
||||||
|
@HiveField(7)
|
||||||
|
final String? itemName;
|
||||||
|
|
||||||
|
/// Product image URL from cart API
|
||||||
|
@HiveField(8)
|
||||||
|
final String? image;
|
||||||
|
|
||||||
|
/// Conversion factor (m² to tiles) from cart API
|
||||||
|
@HiveField(9)
|
||||||
|
final double? conversionOfSm;
|
||||||
|
|
||||||
factory CartItemModel.fromJson(Map<String, dynamic> json) {
|
factory CartItemModel.fromJson(Map<String, dynamic> json) {
|
||||||
return CartItemModel(
|
return CartItemModel(
|
||||||
cartItemId: json['cart_item_id'] as String,
|
cartItemId: json['cart_item_id'] as String,
|
||||||
@@ -67,6 +85,9 @@ class CartItemModel extends HiveObject {
|
|||||||
double? unitPrice,
|
double? unitPrice,
|
||||||
double? subtotal,
|
double? subtotal,
|
||||||
DateTime? addedAt,
|
DateTime? addedAt,
|
||||||
|
String? itemName,
|
||||||
|
String? image,
|
||||||
|
double? conversionOfSm,
|
||||||
}) => CartItemModel(
|
}) => CartItemModel(
|
||||||
cartItemId: cartItemId ?? this.cartItemId,
|
cartItemId: cartItemId ?? this.cartItemId,
|
||||||
cartId: cartId ?? this.cartId,
|
cartId: cartId ?? this.cartId,
|
||||||
@@ -75,5 +96,22 @@ class CartItemModel extends HiveObject {
|
|||||||
unitPrice: unitPrice ?? this.unitPrice,
|
unitPrice: unitPrice ?? this.unitPrice,
|
||||||
subtotal: subtotal ?? this.subtotal,
|
subtotal: subtotal ?? this.subtotal,
|
||||||
addedAt: addedAt ?? this.addedAt,
|
addedAt: addedAt ?? this.addedAt,
|
||||||
|
itemName: itemName ?? this.itemName,
|
||||||
|
image: image ?? this.image,
|
||||||
|
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Convert to domain entity
|
||||||
|
CartItem toEntity() => CartItem(
|
||||||
|
cartItemId: cartItemId,
|
||||||
|
cartId: cartId,
|
||||||
|
productId: productId,
|
||||||
|
quantity: quantity,
|
||||||
|
unitPrice: unitPrice,
|
||||||
|
subtotal: subtotal,
|
||||||
|
addedAt: addedAt,
|
||||||
|
itemName: itemName,
|
||||||
|
image: image,
|
||||||
|
conversionOfSm: conversionOfSm,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,16 @@ class CartItemModelAdapter extends TypeAdapter<CartItemModel> {
|
|||||||
unitPrice: (fields[4] as num).toDouble(),
|
unitPrice: (fields[4] as num).toDouble(),
|
||||||
subtotal: (fields[5] as num).toDouble(),
|
subtotal: (fields[5] as num).toDouble(),
|
||||||
addedAt: fields[6] as DateTime,
|
addedAt: fields[6] as DateTime,
|
||||||
|
itemName: fields[7] as String?,
|
||||||
|
image: fields[8] as String?,
|
||||||
|
conversionOfSm: (fields[9] as num?)?.toDouble(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, CartItemModel obj) {
|
void write(BinaryWriter writer, CartItemModel obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(7)
|
..writeByte(10)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.cartItemId)
|
..write(obj.cartItemId)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
@@ -44,7 +47,13 @@ class CartItemModelAdapter extends TypeAdapter<CartItemModel> {
|
|||||||
..writeByte(5)
|
..writeByte(5)
|
||||||
..write(obj.subtotal)
|
..write(obj.subtotal)
|
||||||
..writeByte(6)
|
..writeByte(6)
|
||||||
..write(obj.addedAt);
|
..write(obj.addedAt)
|
||||||
|
..writeByte(7)
|
||||||
|
..write(obj.itemName)
|
||||||
|
..writeByte(8)
|
||||||
|
..write(obj.image)
|
||||||
|
..writeByte(9)
|
||||||
|
..write(obj.conversionOfSm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
required List<String> itemIds,
|
required List<String> itemIds,
|
||||||
required List<double> quantities,
|
required List<double> quantities,
|
||||||
required List<double> prices,
|
required List<double> prices,
|
||||||
|
List<double?>? conversionFactors,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
// Validate input
|
// Validate input
|
||||||
@@ -48,11 +49,16 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
// Build API request items
|
// Build API request items
|
||||||
final items = <Map<String, dynamic>>[];
|
final items = <Map<String, dynamic>>[];
|
||||||
for (int i = 0; i < itemIds.length; i++) {
|
for (int i = 0; i < itemIds.length; i++) {
|
||||||
items.add({
|
final item = <String, dynamic>{
|
||||||
'item_id': itemIds[i],
|
'item_id': itemIds[i],
|
||||||
'quantity': quantities[i],
|
'quantity': quantities[i],
|
||||||
'amount': prices[i],
|
'amount': prices[i],
|
||||||
});
|
};
|
||||||
|
// Add conversion_of_sm if provided
|
||||||
|
if (conversionFactors != null && i < conversionFactors.length) {
|
||||||
|
item['conversion_of_sm'] = conversionFactors[i] ?? 0.0;
|
||||||
|
}
|
||||||
|
items.add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try API first
|
// Try API first
|
||||||
@@ -66,6 +72,7 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
productId: itemIds[i],
|
productId: itemIds[i],
|
||||||
quantity: quantities[i],
|
quantity: quantities[i],
|
||||||
unitPrice: prices[i],
|
unitPrice: prices[i],
|
||||||
|
conversionOfSm: conversionFactors?[i],
|
||||||
);
|
);
|
||||||
await _localDataSource.addCartItem(cartItemModel);
|
await _localDataSource.addCartItem(cartItemModel);
|
||||||
}
|
}
|
||||||
@@ -80,6 +87,7 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
productId: itemIds[i],
|
productId: itemIds[i],
|
||||||
quantity: quantities[i],
|
quantity: quantities[i],
|
||||||
unitPrice: prices[i],
|
unitPrice: prices[i],
|
||||||
|
conversionOfSm: conversionFactors?[i],
|
||||||
);
|
);
|
||||||
await _localDataSource.addCartItem(cartItemModel);
|
await _localDataSource.addCartItem(cartItemModel);
|
||||||
}
|
}
|
||||||
@@ -176,6 +184,7 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
required String itemId,
|
required String itemId,
|
||||||
required double quantity,
|
required double quantity,
|
||||||
required double price,
|
required double price,
|
||||||
|
double? conversionFactor,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
// API doesn't have update endpoint, use add with new quantity
|
// API doesn't have update endpoint, use add with new quantity
|
||||||
@@ -184,6 +193,7 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
itemIds: [itemId],
|
itemIds: [itemId],
|
||||||
quantities: [quantity],
|
quantities: [quantity],
|
||||||
prices: [price],
|
prices: [price],
|
||||||
|
conversionFactors: conversionFactor != null ? [conversionFactor] : null,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw UnknownException('Failed to update cart item quantity', e);
|
throw UnknownException('Failed to update cart item quantity', e);
|
||||||
@@ -268,6 +278,9 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
unitPrice: model.unitPrice,
|
unitPrice: model.unitPrice,
|
||||||
subtotal: model.subtotal,
|
subtotal: model.subtotal,
|
||||||
addedAt: model.addedAt,
|
addedAt: model.addedAt,
|
||||||
|
itemName: model.itemName,
|
||||||
|
image: model.image,
|
||||||
|
conversionOfSm: model.conversionOfSm,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +289,7 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
required String productId,
|
required String productId,
|
||||||
required double quantity,
|
required double quantity,
|
||||||
required double unitPrice,
|
required double unitPrice,
|
||||||
|
double? conversionOfSm,
|
||||||
}) {
|
}) {
|
||||||
return CartItemModel(
|
return CartItemModel(
|
||||||
cartItemId: DateTime.now().millisecondsSinceEpoch.toString(),
|
cartItemId: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
@@ -285,6 +299,7 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
unitPrice: unitPrice,
|
unitPrice: unitPrice,
|
||||||
subtotal: quantity * unitPrice,
|
subtotal: quantity * unitPrice,
|
||||||
addedAt: DateTime.now(),
|
addedAt: DateTime.now(),
|
||||||
|
conversionOfSm: conversionOfSm,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ library;
|
|||||||
/// Cart Item Entity
|
/// Cart Item Entity
|
||||||
///
|
///
|
||||||
/// Contains item-level information:
|
/// Contains item-level information:
|
||||||
/// - Product reference
|
/// - Product reference and basic info
|
||||||
/// - Quantity
|
/// - Quantity
|
||||||
/// - Pricing
|
/// - Pricing
|
||||||
class CartItem {
|
class CartItem {
|
||||||
@@ -31,6 +31,15 @@ class CartItem {
|
|||||||
/// Timestamp when item was added
|
/// Timestamp when item was added
|
||||||
final DateTime addedAt;
|
final DateTime addedAt;
|
||||||
|
|
||||||
|
/// Product name from cart API
|
||||||
|
final String? itemName;
|
||||||
|
|
||||||
|
/// Product image URL from cart API
|
||||||
|
final String? image;
|
||||||
|
|
||||||
|
/// Conversion factor (m² to tiles) from cart API
|
||||||
|
final double? conversionOfSm;
|
||||||
|
|
||||||
const CartItem({
|
const CartItem({
|
||||||
required this.cartItemId,
|
required this.cartItemId,
|
||||||
required this.cartId,
|
required this.cartId,
|
||||||
@@ -39,6 +48,9 @@ class CartItem {
|
|||||||
required this.unitPrice,
|
required this.unitPrice,
|
||||||
required this.subtotal,
|
required this.subtotal,
|
||||||
required this.addedAt,
|
required this.addedAt,
|
||||||
|
this.itemName,
|
||||||
|
this.image,
|
||||||
|
this.conversionOfSm,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Calculate subtotal (for verification)
|
/// Calculate subtotal (for verification)
|
||||||
@@ -53,6 +65,9 @@ class CartItem {
|
|||||||
double? unitPrice,
|
double? unitPrice,
|
||||||
double? subtotal,
|
double? subtotal,
|
||||||
DateTime? addedAt,
|
DateTime? addedAt,
|
||||||
|
String? itemName,
|
||||||
|
String? image,
|
||||||
|
double? conversionOfSm,
|
||||||
}) {
|
}) {
|
||||||
return CartItem(
|
return CartItem(
|
||||||
cartItemId: cartItemId ?? this.cartItemId,
|
cartItemId: cartItemId ?? this.cartItemId,
|
||||||
@@ -62,6 +77,9 @@ class CartItem {
|
|||||||
unitPrice: unitPrice ?? this.unitPrice,
|
unitPrice: unitPrice ?? this.unitPrice,
|
||||||
subtotal: subtotal ?? this.subtotal,
|
subtotal: subtotal ?? this.subtotal,
|
||||||
addedAt: addedAt ?? this.addedAt,
|
addedAt: addedAt ?? this.addedAt,
|
||||||
|
itemName: itemName ?? this.itemName,
|
||||||
|
image: image ?? this.image,
|
||||||
|
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ abstract class CartRepository {
|
|||||||
/// [itemIds] - Product ERPNext item codes
|
/// [itemIds] - Product ERPNext item codes
|
||||||
/// [quantities] - Quantities for each item
|
/// [quantities] - Quantities for each item
|
||||||
/// [prices] - Unit prices for each item
|
/// [prices] - Unit prices for each item
|
||||||
|
/// [conversionFactors] - Conversion factors (m² to tiles) for each item
|
||||||
///
|
///
|
||||||
/// Returns true if successful.
|
/// Returns true if successful.
|
||||||
/// Throws exceptions on failure.
|
/// Throws exceptions on failure.
|
||||||
@@ -32,6 +33,7 @@ abstract class CartRepository {
|
|||||||
required List<String> itemIds,
|
required List<String> itemIds,
|
||||||
required List<double> quantities,
|
required List<double> quantities,
|
||||||
required List<double> prices,
|
required List<double> prices,
|
||||||
|
List<double?>? conversionFactors,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Remove items from cart
|
/// Remove items from cart
|
||||||
@@ -55,6 +57,7 @@ abstract class CartRepository {
|
|||||||
/// [itemId] - Product ERPNext item code
|
/// [itemId] - Product ERPNext item code
|
||||||
/// [quantity] - New quantity
|
/// [quantity] - New quantity
|
||||||
/// [price] - Unit price
|
/// [price] - Unit price
|
||||||
|
/// [conversionFactor] - Conversion factor (m² to tiles)
|
||||||
///
|
///
|
||||||
/// Returns true if successful.
|
/// Returns true if successful.
|
||||||
/// Throws exceptions on failure.
|
/// Throws exceptions on failure.
|
||||||
@@ -62,6 +65,7 @@ abstract class CartRepository {
|
|||||||
required String itemId,
|
required String itemId,
|
||||||
required double quantity,
|
required double quantity,
|
||||||
required double price,
|
required double price,
|
||||||
|
double? conversionFactor,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Clear all items from cart
|
/// Clear all items from cart
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
/// Shopping cart screen with selection and checkout.
|
/// Shopping cart screen with selection and checkout.
|
||||||
/// Features expanded item list with total price at bottom.
|
/// Features expanded item list with total price at bottom.
|
||||||
library;
|
library;
|
||||||
|
import 'package:worker/core/utils/extensions.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
@@ -35,14 +35,8 @@ class CartPage extends ConsumerStatefulWidget {
|
|||||||
class _CartPageState extends ConsumerState<CartPage> {
|
class _CartPageState extends ConsumerState<CartPage> {
|
||||||
bool _isSyncing = false;
|
bool _isSyncing = false;
|
||||||
|
|
||||||
@override
|
// Cart is initialized once in home_page.dart at app startup
|
||||||
void initState() {
|
// Provider has keepAlive: true, so no need to reload here
|
||||||
super.initState();
|
|
||||||
// Initialize cart from API on mount
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
ref.read(cartProvider.notifier).initialize();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Sync is handled in PopScope.onPopInvokedWithResult for back navigation
|
// Note: Sync is handled in PopScope.onPopInvokedWithResult for back navigation
|
||||||
// and in checkout button handler for checkout flow.
|
// and in checkout button handler for checkout flow.
|
||||||
@@ -53,11 +47,7 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final cartState = ref.watch(cartProvider);
|
final cartState = ref.watch(cartProvider);
|
||||||
|
|
||||||
final currencyFormatter = NumberFormat.currency(
|
|
||||||
locale: 'vi_VN',
|
|
||||||
symbol: 'đ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
final itemCount = cartState.itemCount;
|
final itemCount = cartState.itemCount;
|
||||||
final hasSelection = cartState.selectedCount > 0;
|
final hasSelection = cartState.selectedCount > 0;
|
||||||
@@ -144,7 +134,11 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
context,
|
context,
|
||||||
cartState,
|
cartState,
|
||||||
ref,
|
ref,
|
||||||
currencyFormatter,
|
NumberFormat.currency(
|
||||||
|
locale: 'vi_VN',
|
||||||
|
symbol: 'đ',
|
||||||
|
decimalDigits: 0,
|
||||||
|
),
|
||||||
hasSelection,
|
hasSelection,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
import 'package:worker/features/cart/presentation/providers/cart_data_providers.dart';
|
import 'package:worker/features/cart/presentation/providers/cart_data_providers.dart';
|
||||||
import 'package:worker/features/cart/presentation/providers/cart_state.dart';
|
import 'package:worker/features/cart/presentation/providers/cart_state.dart';
|
||||||
import 'package:worker/features/products/domain/entities/product.dart';
|
import 'package:worker/features/products/domain/entities/product.dart';
|
||||||
import 'package:worker/features/products/presentation/providers/products_provider.dart';
|
|
||||||
|
|
||||||
part 'cart_provider.g.dart';
|
part 'cart_provider.g.dart';
|
||||||
|
|
||||||
@@ -46,8 +45,12 @@ class Cart extends _$Cart {
|
|||||||
|
|
||||||
/// Initialize cart by loading from API
|
/// Initialize cart by loading from API
|
||||||
///
|
///
|
||||||
/// Call this from UI on mount to load cart items from backend.
|
/// Call this ONCE from HomePage on app startup.
|
||||||
|
/// Cart API returns product details, no need to fetch each product separately.
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
|
// Skip if already loaded
|
||||||
|
if (state.items.isNotEmpty) return;
|
||||||
|
|
||||||
final repository = await ref.read(cartRepositoryProvider.future);
|
final repository = await ref.read(cartRepositoryProvider.future);
|
||||||
|
|
||||||
// Set loading state
|
// Set loading state
|
||||||
@@ -55,6 +58,7 @@ class Cart extends _$Cart {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Load cart items from API (with Hive fallback)
|
// Load cart items from API (with Hive fallback)
|
||||||
|
// Cart API returns: item_code, item_name, image, conversion_of_sm, quantity, amount
|
||||||
final cartItems = await repository.getCartItems();
|
final cartItems = await repository.getCartItems();
|
||||||
|
|
||||||
// Get member tier from user profile
|
// Get member tier from user profile
|
||||||
@@ -63,16 +67,28 @@ class Cart extends _$Cart {
|
|||||||
const memberDiscountPercent = 15.0;
|
const memberDiscountPercent = 15.0;
|
||||||
|
|
||||||
// Convert CartItem entities to CartItemData for UI
|
// Convert CartItem entities to CartItemData for UI
|
||||||
|
// Use product data from cart API directly - no need to fetch each product
|
||||||
final items = <CartItemData>[];
|
final items = <CartItemData>[];
|
||||||
final selectedItems = <String, bool>{};
|
final selectedItems = <String, bool>{};
|
||||||
|
|
||||||
// Fetch product details for each cart item
|
|
||||||
final productsRepository = await ref.read(productsRepositoryProvider.future);
|
|
||||||
|
|
||||||
for (final cartItem in cartItems) {
|
for (final cartItem in cartItems) {
|
||||||
try {
|
// Create minimal Product from cart item data (no need to fetch from API)
|
||||||
// Fetch full product entity from products repository
|
final now = DateTime.now();
|
||||||
final product = await productsRepository.getProductById(cartItem.productId);
|
final product = Product(
|
||||||
|
productId: cartItem.productId,
|
||||||
|
name: cartItem.itemName ?? cartItem.productId,
|
||||||
|
basePrice: cartItem.unitPrice,
|
||||||
|
images: cartItem.image != null ? [cartItem.image!] : [],
|
||||||
|
thumbnail: cartItem.image ?? '',
|
||||||
|
imageCaptions: const {},
|
||||||
|
specifications: const {},
|
||||||
|
conversionOfSm: cartItem.conversionOfSm,
|
||||||
|
erpnextItemCode: cartItem.productId,
|
||||||
|
isActive: true,
|
||||||
|
isFeatured: false,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
|
||||||
// Calculate conversion for this item
|
// Calculate conversion for this item
|
||||||
final converted = _calculateConversion(
|
final converted = _calculateConversion(
|
||||||
@@ -80,7 +96,7 @@ class Cart extends _$Cart {
|
|||||||
product.conversionOfSm,
|
product.conversionOfSm,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create CartItemData with full product info
|
// Create CartItemData with product info from cart API
|
||||||
items.add(
|
items.add(
|
||||||
CartItemData(
|
CartItemData(
|
||||||
product: product,
|
product: product,
|
||||||
@@ -92,12 +108,6 @@ class Cart extends _$Cart {
|
|||||||
|
|
||||||
// Initialize as not selected by default
|
// Initialize as not selected by default
|
||||||
selectedItems[product.productId] = false;
|
selectedItems[product.productId] = false;
|
||||||
} catch (productError) {
|
|
||||||
// Skip this item if product can't be fetched
|
|
||||||
// In production, use a proper logging framework
|
|
||||||
// ignore: avoid_print
|
|
||||||
print('[CartProvider] Failed to load product ${cartItem.productId}: $productError');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final newState = CartState(
|
final newState = CartState(
|
||||||
@@ -150,6 +160,7 @@ class Cart extends _$Cart {
|
|||||||
itemIds: [product.erpnextItemCode ?? product.productId],
|
itemIds: [product.erpnextItemCode ?? product.productId],
|
||||||
quantities: [quantity],
|
quantities: [quantity],
|
||||||
prices: [product.basePrice],
|
prices: [product.basePrice],
|
||||||
|
conversionFactors: [product.conversionOfSm],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate conversion
|
// Calculate conversion
|
||||||
@@ -332,6 +343,7 @@ class Cart extends _$Cart {
|
|||||||
itemId: item.product.erpnextItemCode ?? productId,
|
itemId: item.product.erpnextItemCode ?? productId,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
price: item.product.basePrice,
|
price: item.product.basePrice,
|
||||||
|
conversionFactor: item.product.conversionOfSm,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent fail - keep local state, user can retry later
|
// Silent fail - keep local state, user can retry later
|
||||||
@@ -370,6 +382,7 @@ class Cart extends _$Cart {
|
|||||||
itemId: item.product.erpnextItemCode ?? productId,
|
itemId: item.product.erpnextItemCode ?? productId,
|
||||||
quantity: newQuantity,
|
quantity: newQuantity,
|
||||||
price: item.product.basePrice,
|
price: item.product.basePrice,
|
||||||
|
conversionFactor: item.product.conversionOfSm,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update local state
|
// Update local state
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ final class CartProvider extends $NotifierProvider<Cart, CartState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$cartHash() => r'706de28734e7059b2e9484f3b1d94226a0e90bb9';
|
String _$cartHash() => r'a12712db833127ef66b75f52cba8376409806afa';
|
||||||
|
|
||||||
/// Cart Notifier
|
/// Cart Notifier
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:worker/core/utils/extensions.dart';
|
||||||
import 'package:worker/core/widgets/loading_indicator.dart';
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
import 'package:worker/core/theme/typography.dart';
|
import 'package:worker/core/theme/typography.dart';
|
||||||
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
|
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
|
||||||
@@ -79,11 +80,6 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
|
|||||||
final isSelected =
|
final isSelected =
|
||||||
cartState.selectedItems[widget.item.product.productId] ?? false;
|
cartState.selectedItems[widget.item.product.productId] ?? false;
|
||||||
|
|
||||||
final currencyFormatter = NumberFormat.currency(
|
|
||||||
locale: 'vi_VN',
|
|
||||||
symbol: 'đ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
@@ -121,7 +117,11 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: widget.item.product.thumbnail,
|
imageUrl: widget.item.product.thumbnail.isNotEmpty
|
||||||
|
? widget.item.product.thumbnail
|
||||||
|
: (widget.item.product.images.isNotEmpty
|
||||||
|
? widget.item.product.images.first
|
||||||
|
: ''),
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@@ -168,7 +168,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
|
|||||||
|
|
||||||
// Price
|
// Price
|
||||||
Text(
|
Text(
|
||||||
'${currencyFormatter.format(widget.item.product.basePrice)}/m²',
|
'${widget.item.product.basePrice.toVNCurrency}/m²',
|
||||||
style: AppTypography.titleMedium.copyWith(
|
style: AppTypography.titleMedium.copyWith(
|
||||||
color: colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
@@ -60,6 +60,48 @@ class FavoriteProductsLocalDataSource {
|
|||||||
bool isBoxOpen() {
|
bool isBoxOpen() {
|
||||||
return Hive.isBoxOpen(HiveBoxNames.favoriteProductsBox);
|
return Hive.isBoxOpen(HiveBoxNames.favoriteProductsBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a product is in favorites (local only - no API call)
|
||||||
|
bool isFavorite(String productId) {
|
||||||
|
try {
|
||||||
|
return _box.containsKey(productId);
|
||||||
|
} catch (e) {
|
||||||
|
_debugPrint('Error checking favorite: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all favorite product IDs (local only - no API call)
|
||||||
|
Set<String> getFavoriteIds() {
|
||||||
|
try {
|
||||||
|
return _box.keys.cast<String>().toSet();
|
||||||
|
} catch (e) {
|
||||||
|
_debugPrint('Error getting favorite IDs: $e');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a product to local favorites cache
|
||||||
|
Future<void> addFavorite(ProductModel product) async {
|
||||||
|
try {
|
||||||
|
await _box.put(product.productId, product);
|
||||||
|
_debugPrint('Added to local favorites: ${product.productId}');
|
||||||
|
} catch (e) {
|
||||||
|
_debugPrint('Error adding to local favorites: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a product from local favorites cache
|
||||||
|
Future<void> removeFavorite(String productId) async {
|
||||||
|
try {
|
||||||
|
await _box.delete(productId);
|
||||||
|
_debugPrint('Removed from local favorites: $productId');
|
||||||
|
} catch (e) {
|
||||||
|
_debugPrint('Error removing from local favorites: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Debug print helper
|
/// Debug print helper
|
||||||
|
|||||||
@@ -71,7 +71,12 @@ class FavoriteProducts extends _$FavoriteProducts {
|
|||||||
@override
|
@override
|
||||||
Future<List<Product>> build() async {
|
Future<List<Product>> build() async {
|
||||||
_repository = await ref.read(favoritesRepositoryProvider.future);
|
_repository = await ref.read(favoritesRepositoryProvider.future);
|
||||||
return await _loadProducts();
|
final products = await _loadProducts();
|
||||||
|
|
||||||
|
// Sync local IDs after loading
|
||||||
|
ref.read(favoriteIdsLocalProvider.notifier).refresh();
|
||||||
|
|
||||||
|
return products;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -99,20 +104,22 @@ class FavoriteProducts extends _$FavoriteProducts {
|
|||||||
|
|
||||||
/// Add a product to favorites
|
/// Add a product to favorites
|
||||||
///
|
///
|
||||||
/// Calls API to add to wishlist, then refreshes the products list.
|
/// Calls API to add to wishlist, updates local state only (no refetch).
|
||||||
/// No userId needed - the API uses the authenticated session.
|
/// No userId needed - the API uses the authenticated session.
|
||||||
Future<void> addFavorite(String productId) async {
|
Future<void> addFavorite(String productId) async {
|
||||||
try {
|
try {
|
||||||
_debugPrint('Adding product to favorites: $productId');
|
_debugPrint('Adding product to favorites: $productId');
|
||||||
|
|
||||||
|
// Optimistically update local state first for instant UI feedback
|
||||||
|
ref.read(favoriteIdsLocalProvider.notifier).addId(productId);
|
||||||
|
|
||||||
// Call repository to add to favorites (uses auth token from session)
|
// Call repository to add to favorites (uses auth token from session)
|
||||||
await _repository.addFavorite(productId);
|
await _repository.addFavorite(productId);
|
||||||
|
|
||||||
// Refresh the products list after successful addition
|
|
||||||
await refresh();
|
|
||||||
|
|
||||||
_debugPrint('Successfully added favorite: $productId');
|
_debugPrint('Successfully added favorite: $productId');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Rollback optimistic update on error
|
||||||
|
ref.read(favoriteIdsLocalProvider.notifier).removeId(productId);
|
||||||
_debugPrint('Error adding favorite: $e');
|
_debugPrint('Error adding favorite: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -120,20 +127,22 @@ class FavoriteProducts extends _$FavoriteProducts {
|
|||||||
|
|
||||||
/// Remove a product from favorites
|
/// Remove a product from favorites
|
||||||
///
|
///
|
||||||
/// Calls API to remove from wishlist, then refreshes the products list.
|
/// Calls API to remove from wishlist, updates local state only (no refetch).
|
||||||
/// No userId needed - the API uses the authenticated session.
|
/// No userId needed - the API uses the authenticated session.
|
||||||
Future<void> removeFavorite(String productId) async {
|
Future<void> removeFavorite(String productId) async {
|
||||||
try {
|
try {
|
||||||
_debugPrint('Removing product from favorites: $productId');
|
_debugPrint('Removing product from favorites: $productId');
|
||||||
|
|
||||||
|
// Optimistically update local state first for instant UI feedback
|
||||||
|
ref.read(favoriteIdsLocalProvider.notifier).removeId(productId);
|
||||||
|
|
||||||
// Call repository to remove from favorites (uses auth token from session)
|
// Call repository to remove from favorites (uses auth token from session)
|
||||||
await _repository.removeFavorite(productId);
|
await _repository.removeFavorite(productId);
|
||||||
|
|
||||||
// Refresh the products list after successful removal
|
|
||||||
await refresh();
|
|
||||||
|
|
||||||
_debugPrint('Successfully removed favorite: $productId');
|
_debugPrint('Successfully removed favorite: $productId');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Rollback optimistic update on error
|
||||||
|
ref.read(favoriteIdsLocalProvider.notifier).addId(productId);
|
||||||
_debugPrint('Error removing favorite: $e');
|
_debugPrint('Error removing favorite: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -143,9 +152,11 @@ class FavoriteProducts extends _$FavoriteProducts {
|
|||||||
///
|
///
|
||||||
/// If the product is favorited, it will be removed.
|
/// If the product is favorited, it will be removed.
|
||||||
/// If the product is not favorited, it will be added.
|
/// If the product is not favorited, it will be added.
|
||||||
|
/// Checks from local state for instant response.
|
||||||
Future<void> toggleFavorite(String productId) async {
|
Future<void> toggleFavorite(String productId) async {
|
||||||
final currentProducts = state.value ?? [];
|
// Check from local IDs (instant, no API call)
|
||||||
final isFavorited = currentProducts.any((p) => p.productId == productId);
|
final localIds = ref.read(favoriteIdsLocalProvider);
|
||||||
|
final isFavorited = localIds.contains(productId);
|
||||||
|
|
||||||
if (isFavorited) {
|
if (isFavorited) {
|
||||||
await removeFavorite(productId);
|
await removeFavorite(productId);
|
||||||
@@ -170,20 +181,48 @@ class FavoriteProducts extends _$FavoriteProducts {
|
|||||||
// HELPER PROVIDERS
|
// HELPER PROVIDERS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Check if a specific product is favorited
|
/// Check if a specific product is favorited (LOCAL ONLY - no API call)
|
||||||
///
|
///
|
||||||
/// Derived from the favorite products list.
|
/// Reads directly from Hive local cache for instant response.
|
||||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
/// This is used in product detail page to avoid unnecessary API calls.
|
||||||
/// Safe to use in build methods - will return false during loading/error states.
|
/// The cache is synced when favorites are loaded or modified.
|
||||||
@riverpod
|
@riverpod
|
||||||
bool isFavorite(Ref ref, String productId) {
|
bool isFavorite(Ref ref, String productId) {
|
||||||
final favoriteProductsAsync = ref.watch(favoriteProductsProvider);
|
// Watch the notifier state to trigger rebuild when favorites change
|
||||||
|
// But check from local Hive directly for instant response
|
||||||
|
ref.watch(favoriteIdsLocalProvider);
|
||||||
|
|
||||||
return favoriteProductsAsync.when(
|
final localDataSource = ref.read(favoriteProductsLocalDataSourceProvider);
|
||||||
data: (products) => products.any((p) => p.productId == productId),
|
return localDataSource.isFavorite(productId);
|
||||||
loading: () => false,
|
}
|
||||||
error: (_, __) => false,
|
|
||||||
);
|
/// Local favorite IDs provider (synced with Hive)
|
||||||
|
///
|
||||||
|
/// This provider watches Hive changes and provides a Set of favorite product IDs.
|
||||||
|
/// Used to trigger rebuilds when favorites are added/removed.
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class FavoriteIdsLocal extends _$FavoriteIdsLocal {
|
||||||
|
@override
|
||||||
|
Set<String> build() {
|
||||||
|
final localDataSource = ref.read(favoriteProductsLocalDataSourceProvider);
|
||||||
|
return localDataSource.getFavoriteIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh from local storage
|
||||||
|
void refresh() {
|
||||||
|
final localDataSource = ref.read(favoriteProductsLocalDataSourceProvider);
|
||||||
|
state = localDataSource.getFavoriteIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a product ID to local state
|
||||||
|
void addId(String productId) {
|
||||||
|
state = {...state, productId};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a product ID from local state
|
||||||
|
void removeId(String productId) {
|
||||||
|
state = {...state}..remove(productId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the total count of favorites
|
/// Get the total count of favorites
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ final class FavoriteProductsProvider
|
|||||||
FavoriteProducts create() => FavoriteProducts();
|
FavoriteProducts create() => FavoriteProducts();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$favoriteProductsHash() => r'd43c41db210259021df104f9fecdd00cf474d196';
|
String _$favoriteProductsHash() => r'6d042f469a1f71bb06f8b5b76014bf24e30e6758';
|
||||||
|
|
||||||
/// Manages favorite products with full Product data from wishlist API
|
/// Manages favorite products with full Product data from wishlist API
|
||||||
///
|
///
|
||||||
@@ -269,28 +269,28 @@ abstract class _$FavoriteProducts extends $AsyncNotifier<List<Product>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a specific product is favorited
|
/// Check if a specific product is favorited (LOCAL ONLY - no API call)
|
||||||
///
|
///
|
||||||
/// Derived from the favorite products list.
|
/// Reads directly from Hive local cache for instant response.
|
||||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
/// This is used in product detail page to avoid unnecessary API calls.
|
||||||
/// Safe to use in build methods - will return false during loading/error states.
|
/// The cache is synced when favorites are loaded or modified.
|
||||||
|
|
||||||
@ProviderFor(isFavorite)
|
@ProviderFor(isFavorite)
|
||||||
const isFavoriteProvider = IsFavoriteFamily._();
|
const isFavoriteProvider = IsFavoriteFamily._();
|
||||||
|
|
||||||
/// Check if a specific product is favorited
|
/// Check if a specific product is favorited (LOCAL ONLY - no API call)
|
||||||
///
|
///
|
||||||
/// Derived from the favorite products list.
|
/// Reads directly from Hive local cache for instant response.
|
||||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
/// This is used in product detail page to avoid unnecessary API calls.
|
||||||
/// Safe to use in build methods - will return false during loading/error states.
|
/// The cache is synced when favorites are loaded or modified.
|
||||||
|
|
||||||
final class IsFavoriteProvider extends $FunctionalProvider<bool, bool, bool>
|
final class IsFavoriteProvider extends $FunctionalProvider<bool, bool, bool>
|
||||||
with $Provider<bool> {
|
with $Provider<bool> {
|
||||||
/// Check if a specific product is favorited
|
/// Check if a specific product is favorited (LOCAL ONLY - no API call)
|
||||||
///
|
///
|
||||||
/// Derived from the favorite products list.
|
/// Reads directly from Hive local cache for instant response.
|
||||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
/// This is used in product detail page to avoid unnecessary API calls.
|
||||||
/// Safe to use in build methods - will return false during loading/error states.
|
/// The cache is synced when favorites are loaded or modified.
|
||||||
const IsFavoriteProvider._({
|
const IsFavoriteProvider._({
|
||||||
required IsFavoriteFamily super.from,
|
required IsFavoriteFamily super.from,
|
||||||
required String super.argument,
|
required String super.argument,
|
||||||
@@ -342,13 +342,13 @@ final class IsFavoriteProvider extends $FunctionalProvider<bool, bool, bool>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$isFavoriteHash() => r'6e2f5a50d2350975e17d91f395595cd284b69c20';
|
String _$isFavoriteHash() => r'7aa2377f37ceb2c450c9e29b5c134ba160e4ecc2';
|
||||||
|
|
||||||
/// Check if a specific product is favorited
|
/// Check if a specific product is favorited (LOCAL ONLY - no API call)
|
||||||
///
|
///
|
||||||
/// Derived from the favorite products list.
|
/// Reads directly from Hive local cache for instant response.
|
||||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
/// This is used in product detail page to avoid unnecessary API calls.
|
||||||
/// Safe to use in build methods - will return false during loading/error states.
|
/// The cache is synced when favorites are loaded or modified.
|
||||||
|
|
||||||
final class IsFavoriteFamily extends $Family
|
final class IsFavoriteFamily extends $Family
|
||||||
with $FunctionalFamilyOverride<bool, String> {
|
with $FunctionalFamilyOverride<bool, String> {
|
||||||
@@ -361,11 +361,11 @@ final class IsFavoriteFamily extends $Family
|
|||||||
isAutoDispose: true,
|
isAutoDispose: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Check if a specific product is favorited
|
/// Check if a specific product is favorited (LOCAL ONLY - no API call)
|
||||||
///
|
///
|
||||||
/// Derived from the favorite products list.
|
/// Reads directly from Hive local cache for instant response.
|
||||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
/// This is used in product detail page to avoid unnecessary API calls.
|
||||||
/// Safe to use in build methods - will return false during loading/error states.
|
/// The cache is synced when favorites are loaded or modified.
|
||||||
|
|
||||||
IsFavoriteProvider call(String productId) =>
|
IsFavoriteProvider call(String productId) =>
|
||||||
IsFavoriteProvider._(argument: productId, from: this);
|
IsFavoriteProvider._(argument: productId, from: this);
|
||||||
@@ -374,6 +374,77 @@ final class IsFavoriteFamily extends $Family
|
|||||||
String toString() => r'isFavoriteProvider';
|
String toString() => r'isFavoriteProvider';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Local favorite IDs provider (synced with Hive)
|
||||||
|
///
|
||||||
|
/// This provider watches Hive changes and provides a Set of favorite product IDs.
|
||||||
|
/// Used to trigger rebuilds when favorites are added/removed.
|
||||||
|
|
||||||
|
@ProviderFor(FavoriteIdsLocal)
|
||||||
|
const favoriteIdsLocalProvider = FavoriteIdsLocalProvider._();
|
||||||
|
|
||||||
|
/// Local favorite IDs provider (synced with Hive)
|
||||||
|
///
|
||||||
|
/// This provider watches Hive changes and provides a Set of favorite product IDs.
|
||||||
|
/// Used to trigger rebuilds when favorites are added/removed.
|
||||||
|
final class FavoriteIdsLocalProvider
|
||||||
|
extends $NotifierProvider<FavoriteIdsLocal, Set<String>> {
|
||||||
|
/// Local favorite IDs provider (synced with Hive)
|
||||||
|
///
|
||||||
|
/// This provider watches Hive changes and provides a Set of favorite product IDs.
|
||||||
|
/// Used to trigger rebuilds when favorites are added/removed.
|
||||||
|
const FavoriteIdsLocalProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'favoriteIdsLocalProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$favoriteIdsLocalHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
FavoriteIdsLocal create() => FavoriteIdsLocal();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(Set<String> value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<Set<String>>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$favoriteIdsLocalHash() => r'db248bc6dcd8ba39d8c3e410188cac67ebf96140';
|
||||||
|
|
||||||
|
/// Local favorite IDs provider (synced with Hive)
|
||||||
|
///
|
||||||
|
/// This provider watches Hive changes and provides a Set of favorite product IDs.
|
||||||
|
/// Used to trigger rebuilds when favorites are added/removed.
|
||||||
|
|
||||||
|
abstract class _$FavoriteIdsLocal extends $Notifier<Set<String>> {
|
||||||
|
Set<String> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<Set<String>, Set<String>>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<Set<String>, Set<String>>,
|
||||||
|
Set<String>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the total count of favorites
|
/// Get the total count of favorites
|
||||||
///
|
///
|
||||||
/// Derived from the favorite products list.
|
/// Derived from the favorite products list.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ part of 'member_card_provider.dart';
|
|||||||
///
|
///
|
||||||
/// memberCardAsync.when(
|
/// memberCardAsync.when(
|
||||||
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
|
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -40,7 +40,7 @@ const memberCardProvider = MemberCardNotifierProvider._();
|
|||||||
///
|
///
|
||||||
/// memberCardAsync.when(
|
/// memberCardAsync.when(
|
||||||
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
|
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -58,7 +58,7 @@ final class MemberCardNotifierProvider
|
|||||||
///
|
///
|
||||||
/// memberCardAsync.when(
|
/// memberCardAsync.when(
|
||||||
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
|
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -96,7 +96,7 @@ String _$memberCardNotifierHash() =>
|
|||||||
///
|
///
|
||||||
/// memberCardAsync.when(
|
/// memberCardAsync.when(
|
||||||
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
|
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ part of 'promotions_provider.dart';
|
|||||||
///
|
///
|
||||||
/// promotionsAsync.when(
|
/// promotionsAsync.when(
|
||||||
/// data: (promotions) => PromotionSlider(promotions: promotions),
|
/// data: (promotions) => PromotionSlider(promotions: promotions),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -42,7 +42,7 @@ const promotionsProvider = PromotionsProvider._();
|
|||||||
///
|
///
|
||||||
/// promotionsAsync.when(
|
/// promotionsAsync.when(
|
||||||
/// data: (promotions) => PromotionSlider(promotions: promotions),
|
/// data: (promotions) => PromotionSlider(promotions: promotions),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -68,7 +68,7 @@ final class PromotionsProvider
|
|||||||
///
|
///
|
||||||
/// promotionsAsync.when(
|
/// promotionsAsync.when(
|
||||||
/// data: (promotions) => PromotionSlider(promotions: promotions),
|
/// data: (promotions) => PromotionSlider(promotions: promotions),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ class ProductsRemoteDataSource {
|
|||||||
data: {
|
data: {
|
||||||
'doctype': 'Item Group',
|
'doctype': 'Item Group',
|
||||||
'fields': ['item_group_name', 'name'],
|
'fields': ['item_group_name', 'name'],
|
||||||
'filters': {'is_group': 0},
|
'filters': {'is_group': 0, 'custom_published': 1},
|
||||||
'limit_page_length': 0,
|
'limit_page_length': 0,
|
||||||
},
|
},
|
||||||
options: Options(headers: headers),
|
options: Options(headers: headers),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:worker/core/constants/ui_constants.dart';
|
|||||||
import 'package:worker/core/router/app_router.dart';
|
import 'package:worker/core/router/app_router.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
|
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
|
||||||
|
import 'package:worker/features/products/presentation/providers/product_filters_provider.dart';
|
||||||
import 'package:worker/features/products/presentation/providers/products_provider.dart';
|
import 'package:worker/features/products/presentation/providers/products_provider.dart';
|
||||||
import 'package:worker/features/products/presentation/widgets/brand_filter_chips.dart';
|
import 'package:worker/features/products/presentation/widgets/brand_filter_chips.dart';
|
||||||
import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart';
|
import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart';
|
||||||
@@ -100,6 +101,8 @@ class ProductsPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
// Sync pending filters with applied filters before opening drawer
|
||||||
|
ref.read(productFiltersProvider.notifier).syncWithApplied();
|
||||||
// Open filter drawer from right
|
// Open filter drawer from right
|
||||||
Scaffold.of(scaffoldContext).openEndDrawer();
|
Scaffold.of(scaffoldContext).openEndDrawer();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ part of 'product_filter_options_provider.dart';
|
|||||||
///
|
///
|
||||||
/// filterOptionsAsync.when(
|
/// filterOptionsAsync.when(
|
||||||
/// data: (options) => ProductFilterDrawer(options: options),
|
/// data: (options) => ProductFilterDrawer(options: options),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -48,7 +48,7 @@ const productFilterOptionsProvider = ProductFilterOptionsProvider._();
|
|||||||
///
|
///
|
||||||
/// filterOptionsAsync.when(
|
/// filterOptionsAsync.when(
|
||||||
/// data: (options) => ProductFilterDrawer(options: options),
|
/// data: (options) => ProductFilterDrawer(options: options),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -79,7 +79,7 @@ final class ProductFilterOptionsProvider
|
|||||||
///
|
///
|
||||||
/// filterOptionsAsync.when(
|
/// filterOptionsAsync.when(
|
||||||
/// data: (options) => ProductFilterDrawer(options: options),
|
/// data: (options) => ProductFilterDrawer(options: options),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/// Provider: Product Filters State
|
/// Provider: Product Filters State
|
||||||
///
|
///
|
||||||
/// Manages product filter selections.
|
/// Manages product filter selections with separate pending and applied states.
|
||||||
|
/// Pending filters: Updated on every checkbox toggle (no API call)
|
||||||
|
/// Applied filters: Only updated when Apply button is pressed (triggers API)
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@@ -54,7 +56,10 @@ class ProductFiltersState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Product Filters Notifier
|
/// Product Filters Notifier (Pending Filters - for UI selection)
|
||||||
|
///
|
||||||
|
/// This provider stores the PENDING filter selections in the drawer.
|
||||||
|
/// Changes here do NOT trigger API calls.
|
||||||
class ProductFiltersNotifier extends Notifier<ProductFiltersState> {
|
class ProductFiltersNotifier extends Notifier<ProductFiltersState> {
|
||||||
@override
|
@override
|
||||||
ProductFiltersState build() => const ProductFiltersState();
|
ProductFiltersState build() => const ProductFiltersState();
|
||||||
@@ -114,19 +119,52 @@ class ProductFiltersNotifier extends Notifier<ProductFiltersState> {
|
|||||||
state = state.copyWith(brands: newSet);
|
state = state.copyWith(brands: newSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset all filters
|
/// Reset all filters (both pending and applied)
|
||||||
void reset() {
|
void reset() {
|
||||||
state = const ProductFiltersState();
|
state = const ProductFiltersState();
|
||||||
|
// Also reset applied filters
|
||||||
|
ref.read(appliedProductFiltersProvider.notifier).reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply filters (placeholder for future implementation)
|
/// Apply filters - copies pending state to applied state
|
||||||
|
/// This is the ONLY action that triggers API calls
|
||||||
void apply() {
|
void apply() {
|
||||||
// TODO: Trigger products provider refresh with filters
|
ref.read(appliedProductFiltersProvider.notifier).applyFilters(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sync pending state with applied state (when opening drawer)
|
||||||
|
void syncWithApplied() {
|
||||||
|
state = ref.read(appliedProductFiltersProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Product Filters Provider
|
/// Applied Product Filters Notifier (Triggers API calls)
|
||||||
|
///
|
||||||
|
/// This provider stores the APPLIED filter state.
|
||||||
|
/// The products provider watches THIS provider, not the pending one.
|
||||||
|
class AppliedProductFiltersNotifier extends Notifier<ProductFiltersState> {
|
||||||
|
@override
|
||||||
|
ProductFiltersState build() => const ProductFiltersState();
|
||||||
|
|
||||||
|
/// Apply filters from pending state
|
||||||
|
void applyFilters(ProductFiltersState filters) {
|
||||||
|
state = filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset applied filters
|
||||||
|
void reset() {
|
||||||
|
state = const ProductFiltersState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Product Filters Provider (Pending - for drawer UI)
|
||||||
final productFiltersProvider =
|
final productFiltersProvider =
|
||||||
NotifierProvider<ProductFiltersNotifier, ProductFiltersState>(
|
NotifierProvider<ProductFiltersNotifier, ProductFiltersState>(
|
||||||
ProductFiltersNotifier.new,
|
ProductFiltersNotifier.new,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Applied Product Filters Provider (Triggers API)
|
||||||
|
final appliedProductFiltersProvider =
|
||||||
|
NotifierProvider<AppliedProductFiltersNotifier, ProductFiltersState>(
|
||||||
|
AppliedProductFiltersNotifier.new,
|
||||||
|
);
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ class Products extends _$Products {
|
|||||||
|
|
||||||
// Watch dependencies (triggers rebuild when they change)
|
// Watch dependencies (triggers rebuild when they change)
|
||||||
final searchQuery = ref.watch(searchQueryProvider);
|
final searchQuery = ref.watch(searchQueryProvider);
|
||||||
final filters = ref.watch(productFiltersProvider);
|
// Watch APPLIED filters, not pending filters (API only called on Apply)
|
||||||
|
final filters = ref.watch(appliedProductFiltersProvider);
|
||||||
|
|
||||||
// Get repository with injected data sources
|
// Get repository with injected data sources
|
||||||
final repository = await ref.watch(productsRepositoryProvider.future);
|
final repository = await ref.watch(productsRepositoryProvider.future);
|
||||||
@@ -148,7 +149,8 @@ class Products extends _$Products {
|
|||||||
|
|
||||||
// Read dependencies to get current filters (use read, not watch)
|
// Read dependencies to get current filters (use read, not watch)
|
||||||
final searchQuery = ref.read(searchQueryProvider);
|
final searchQuery = ref.read(searchQueryProvider);
|
||||||
final filters = ref.read(productFiltersProvider);
|
// Read APPLIED filters, not pending filters
|
||||||
|
final filters = ref.read(appliedProductFiltersProvider);
|
||||||
|
|
||||||
// Get repository
|
// Get repository
|
||||||
final repository = await ref.read(productsRepositoryProvider.future);
|
final repository = await ref.read(productsRepositoryProvider.future);
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ String _$productsRepositoryHash() =>
|
|||||||
///
|
///
|
||||||
/// productsAsync.when(
|
/// productsAsync.when(
|
||||||
/// data: (products) => ProductGrid(products: products),
|
/// data: (products) => ProductGrid(products: products),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -187,7 +187,7 @@ const productsProvider = ProductsProvider._();
|
|||||||
///
|
///
|
||||||
/// productsAsync.when(
|
/// productsAsync.when(
|
||||||
/// data: (products) => ProductGrid(products: products),
|
/// data: (products) => ProductGrid(products: products),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -205,7 +205,7 @@ final class ProductsProvider
|
|||||||
///
|
///
|
||||||
/// productsAsync.when(
|
/// productsAsync.when(
|
||||||
/// data: (products) => ProductGrid(products: products),
|
/// data: (products) => ProductGrid(products: products),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -228,7 +228,7 @@ final class ProductsProvider
|
|||||||
Products create() => Products();
|
Products create() => Products();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$productsHash() => r'a4f416712cdbf2e633622c65b1fdc95686e31fa4';
|
String _$productsHash() => r'502af6c2e9012a619c15fd04bfe778045739e247';
|
||||||
|
|
||||||
/// Products Provider
|
/// Products Provider
|
||||||
///
|
///
|
||||||
@@ -242,7 +242,7 @@ String _$productsHash() => r'a4f416712cdbf2e633622c65b1fdc95686e31fa4';
|
|||||||
///
|
///
|
||||||
/// productsAsync.when(
|
/// productsAsync.when(
|
||||||
/// data: (products) => ProductGrid(products: products),
|
/// data: (products) => ProductGrid(products: products),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -333,7 +333,7 @@ String _$allProductsHash() => r'402d7c6e8d119c7c7eab5e696fb8163831259def';
|
|||||||
///
|
///
|
||||||
/// productAsync.when(
|
/// productAsync.when(
|
||||||
/// data: (product) => ProductDetailView(product: product),
|
/// data: (product) => ProductDetailView(product: product),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -352,7 +352,7 @@ const productDetailProvider = ProductDetailFamily._();
|
|||||||
///
|
///
|
||||||
/// productAsync.when(
|
/// productAsync.when(
|
||||||
/// data: (product) => ProductDetailView(product: product),
|
/// data: (product) => ProductDetailView(product: product),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -371,7 +371,7 @@ final class ProductDetailProvider
|
|||||||
///
|
///
|
||||||
/// productAsync.when(
|
/// productAsync.when(
|
||||||
/// data: (product) => ProductDetailView(product: product),
|
/// data: (product) => ProductDetailView(product: product),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -431,7 +431,7 @@ String _$productDetailHash() => r'ca219f1451f518c84ca1832aacb3c83920f4bfd2';
|
|||||||
///
|
///
|
||||||
/// productAsync.when(
|
/// productAsync.when(
|
||||||
/// data: (product) => ProductDetailView(product: product),
|
/// data: (product) => ProductDetailView(product: product),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -458,7 +458,7 @@ final class ProductDetailFamily extends $Family
|
|||||||
///
|
///
|
||||||
/// productAsync.when(
|
/// productAsync.when(
|
||||||
/// data: (product) => ProductDetailView(product: product),
|
/// data: (product) => ProductDetailView(product: product),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -13,15 +13,16 @@ import 'package:worker/features/products/presentation/providers/product_filters_
|
|||||||
/// Brand Filter Chips Widget
|
/// Brand Filter Chips Widget
|
||||||
///
|
///
|
||||||
/// Displays brands as horizontally scrolling chips.
|
/// Displays brands as horizontally scrolling chips.
|
||||||
/// Synced with filter drawer - both use productFiltersProvider.brands.
|
/// Watches appliedProductFiltersProvider to show currently active filters.
|
||||||
/// Chips are single-select (tapping a brand clears others and sets just that one).
|
/// Tapping a chip directly applies the filter (triggers API call immediately).
|
||||||
class BrandFilterChips extends ConsumerWidget {
|
class BrandFilterChips extends ConsumerWidget {
|
||||||
const BrandFilterChips({super.key});
|
const BrandFilterChips({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final filtersState = ref.watch(productFiltersProvider);
|
// Watch APPLIED filters to show current active state
|
||||||
|
final appliedFilters = ref.watch(appliedProductFiltersProvider);
|
||||||
final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
|
final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
|
||||||
|
|
||||||
return filterOptionsAsync.when(
|
return filterOptionsAsync.when(
|
||||||
@@ -46,9 +47,9 @@ class BrandFilterChips extends ConsumerWidget {
|
|||||||
// "Tất cả" is selected if no brands are selected
|
// "Tất cả" is selected if no brands are selected
|
||||||
// A brand chip is selected if it's the ONLY brand selected
|
// A brand chip is selected if it's the ONLY brand selected
|
||||||
final isSelected = brand.value == 'all'
|
final isSelected = brand.value == 'all'
|
||||||
? filtersState.brands.isEmpty
|
? appliedFilters.brands.isEmpty
|
||||||
: (filtersState.brands.length == 1 &&
|
: (appliedFilters.brands.length == 1 &&
|
||||||
filtersState.brands.contains(brand.value));
|
appliedFilters.brands.contains(brand.value));
|
||||||
|
|
||||||
return FilterChip(
|
return FilterChip(
|
||||||
label: Text(
|
label: Text(
|
||||||
@@ -56,33 +57,28 @@ class BrandFilterChips extends ConsumerWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||||
color: colorScheme.onSurface,
|
color: isSelected ? Colors.white : colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
onSelected: (selected) {
|
onSelected: (selected) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
final notifier = ref.read(productFiltersProvider.notifier);
|
// Create new filter state with only the selected brand
|
||||||
|
ProductFiltersState newFilters;
|
||||||
|
|
||||||
if (brand.value == 'all') {
|
if (brand.value == 'all') {
|
||||||
// Clear all brand filters
|
// Clear brand filter, keep other filters
|
||||||
// Reset all brands by setting to empty set
|
newFilters = appliedFilters.copyWith(brands: {});
|
||||||
final currentBrands = List<String>.from(filtersState.brands);
|
|
||||||
for (final b in currentBrands) {
|
|
||||||
notifier.toggleBrand(b); // Toggle off each brand
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Single-select: clear all other brands and set only this one
|
// Set only this brand, keep other filters
|
||||||
final currentBrands = List<String>.from(filtersState.brands);
|
newFilters = appliedFilters.copyWith(brands: {brand.value});
|
||||||
|
|
||||||
// First, clear all existing brands
|
|
||||||
for (final b in currentBrands) {
|
|
||||||
notifier.toggleBrand(b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then add the selected brand
|
// Apply directly to trigger API call
|
||||||
notifier.toggleBrand(brand.value);
|
ref.read(appliedProductFiltersProvider.notifier).applyFilters(newFilters);
|
||||||
}
|
|
||||||
|
// Also sync pending filters with applied
|
||||||
|
ref.read(productFiltersProvider.notifier).syncWithApplied();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backgroundColor: colorScheme.surface,
|
backgroundColor: colorScheme.surface,
|
||||||
|
|||||||
Reference in New Issue
Block a user