add favorite
This commit is contained in:
@@ -145,6 +145,25 @@ class ApiConstants {
|
||||
/// Body: { "method": "whatsapp|telegram|sms" }
|
||||
static const String shareReferral = '/loyalty/referral/share';
|
||||
|
||||
// ============================================================================
|
||||
// Favorites/Wishlist Endpoints (Frappe ERPNext)
|
||||
// ============================================================================
|
||||
|
||||
/// Get favorite/wishlist items for current user
|
||||
/// POST /api/method/building_material.building_material.api.item_wishlist.get_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
static const String getFavorites = '/building_material.building_material.api.item_wishlist.get_list';
|
||||
|
||||
/// Add item to wishlist
|
||||
/// POST /api/method/building_material.building_material.api.item_wishlist.add_to_wishlist
|
||||
/// Body: { "item_id": "GIB20 G04" }
|
||||
static const String addToFavorites = '/building_material.building_material.api.item_wishlist.add_to_wishlist';
|
||||
|
||||
/// Remove item from wishlist
|
||||
/// POST /api/method/building_material.building_material.api.item_wishlist.remove_from_wishlist
|
||||
/// Body: { "item_id": "GIB20 G04" }
|
||||
static const String removeFromFavorites = '/building_material.building_material.api.item_wishlist.remove_from_wishlist';
|
||||
|
||||
// ============================================================================
|
||||
// Product Endpoints
|
||||
// ============================================================================
|
||||
|
||||
@@ -51,8 +51,8 @@ class HiveBoxNames {
|
||||
/// Address book
|
||||
static const String addressBox = 'address_box';
|
||||
|
||||
/// Favorite products
|
||||
static const String favoriteBox = 'favorite_box';
|
||||
/// Favorite products data (cached from wishlist API)
|
||||
static const String favoriteProductsBox = 'favorite_products_box';
|
||||
|
||||
/// Offline request queue for failed API calls
|
||||
static const String offlineQueueBox = 'offline_queue_box';
|
||||
@@ -72,7 +72,7 @@ class HiveBoxNames {
|
||||
syncStateBox,
|
||||
notificationBox,
|
||||
addressBox,
|
||||
favoriteBox,
|
||||
favoriteProductsBox,
|
||||
offlineQueueBox,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
|
||||
import 'package:worker/core/database/database_manager.dart';
|
||||
import 'package:worker/core/database/hive_service.dart';
|
||||
@@ -53,6 +54,9 @@ class HiveInitializer {
|
||||
|
||||
final dbManager = DatabaseManager();
|
||||
|
||||
// Migration: Delete old favoriteBox (deprecated, replaced with favoriteProductsBox)
|
||||
await _deleteLegacyFavoriteBox(verbose);
|
||||
|
||||
// Clear expired cache on app start
|
||||
await dbManager.clearExpiredCache();
|
||||
|
||||
@@ -97,6 +101,33 @@ class HiveInitializer {
|
||||
await hiveService.clearUserData();
|
||||
}
|
||||
|
||||
/// Delete legacy favoriteBox (migration helper)
|
||||
///
|
||||
/// The old favoriteBox stored FavoriteModel which has been removed.
|
||||
/// This method deletes the old box to prevent typeId errors.
|
||||
static Future<void> _deleteLegacyFavoriteBox(bool verbose) async {
|
||||
try {
|
||||
const legacyBoxName = 'favorite_box';
|
||||
|
||||
// Check if the old box exists
|
||||
if (await Hive.boxExists(legacyBoxName)) {
|
||||
if (verbose) {
|
||||
debugPrint('HiveInitializer: Deleting legacy favoriteBox...');
|
||||
}
|
||||
|
||||
// Delete the box from disk
|
||||
await Hive.deleteBoxFromDisk(legacyBoxName);
|
||||
|
||||
if (verbose) {
|
||||
debugPrint('HiveInitializer: Legacy favoriteBox deleted successfully');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('HiveInitializer: Error deleting legacy favoriteBox: $e');
|
||||
// Don't rethrow - this is just a cleanup operation
|
||||
}
|
||||
}
|
||||
|
||||
/// Get database statistics
|
||||
///
|
||||
/// Returns statistics about all Hive boxes.
|
||||
|
||||
@@ -156,8 +156,8 @@ class HiveService {
|
||||
// Notification box (non-sensitive)
|
||||
Hive.openBox<dynamic>(HiveBoxNames.notificationBox),
|
||||
|
||||
// Favorites box (non-sensitive)
|
||||
Hive.openBox<dynamic>(HiveBoxNames.favoriteBox),
|
||||
// Favorite products box (non-sensitive) - caches Product entities from wishlist API
|
||||
Hive.openBox<dynamic>(HiveBoxNames.favoriteProductsBox),
|
||||
]);
|
||||
|
||||
// Open potentially encrypted boxes (sensitive data)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/products/data/models/product_model.dart';
|
||||
|
||||
/// Favorite Products Local DataSource
|
||||
///
|
||||
/// Caches the actual product data from wishlist API.
|
||||
/// This is separate from ProductsLocalDataSource to avoid conflicts.
|
||||
class FavoriteProductsLocalDataSource {
|
||||
/// Get the Hive box for favorite products
|
||||
Box<dynamic> get _box {
|
||||
return Hive.box<dynamic>(HiveBoxNames.favoriteProductsBox);
|
||||
}
|
||||
|
||||
/// Get all favorite products from cache
|
||||
Future<List<ProductModel>> getAllProducts() async {
|
||||
try {
|
||||
final products = _box.values
|
||||
.whereType<ProductModel>()
|
||||
.toList();
|
||||
|
||||
_debugPrint('Loaded ${products.length} favorite products from cache');
|
||||
return products;
|
||||
} catch (e) {
|
||||
_debugPrint('Error getting favorite products: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Save products from wishlist API to cache
|
||||
Future<void> saveProducts(List<ProductModel> products) async {
|
||||
try {
|
||||
// Clear existing cache
|
||||
await _box.clear();
|
||||
|
||||
// Save new products
|
||||
for (final product in products) {
|
||||
await _box.put(product.productId, product);
|
||||
}
|
||||
|
||||
_debugPrint('Cached ${products.length} favorite products');
|
||||
} catch (e) {
|
||||
_debugPrint('Error saving favorite products: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all cached favorite products
|
||||
Future<void> clearAll() async {
|
||||
try {
|
||||
await _box.clear();
|
||||
_debugPrint('Cleared all favorite products cache');
|
||||
} catch (e) {
|
||||
_debugPrint('Error clearing favorite products: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the box is open
|
||||
bool isBoxOpen() {
|
||||
return Hive.isBoxOpen(HiveBoxNames.favoriteProductsBox);
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug print helper
|
||||
void _debugPrint(String message) {
|
||||
// ignore: avoid_print
|
||||
print('[FavoriteProductsLocalDataSource] $message');
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/favorites/data/models/favorite_model.dart';
|
||||
|
||||
/// Favorites Local DataSource
|
||||
///
|
||||
/// Handles all local database operations for favorites using Hive.
|
||||
/// Supports multi-user functionality by filtering favorites by userId.
|
||||
class FavoritesLocalDataSource {
|
||||
/// Get the Hive box for favorites
|
||||
Box<dynamic> get _box {
|
||||
return Hive.box<dynamic>(HiveBoxNames.favoriteBox);
|
||||
}
|
||||
|
||||
/// Get all favorites for a specific user
|
||||
///
|
||||
/// Returns a list of [FavoriteModel] filtered by [userId].
|
||||
/// If the box is not open or an error occurs, returns an empty list.
|
||||
Future<List<FavoriteModel>> getAllFavorites(String userId) async {
|
||||
try {
|
||||
final favorites = _box.values
|
||||
.whereType<FavoriteModel>()
|
||||
.where((fav) => fav.userId == userId)
|
||||
.toList();
|
||||
|
||||
// Sort by creation date (newest first)
|
||||
favorites.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
|
||||
return favorites;
|
||||
} catch (e) {
|
||||
debugPrint('[FavoritesLocalDataSource] Error getting favorites: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a favorite to the database
|
||||
///
|
||||
/// Adds a new [FavoriteModel] to the Hive box.
|
||||
/// Uses the favoriteId as the key for efficient lookup.
|
||||
Future<void> addFavorite(FavoriteModel favorite) async {
|
||||
try {
|
||||
await _box.put(favorite.favoriteId, favorite);
|
||||
debugPrint(
|
||||
'[FavoritesLocalDataSource] Added favorite: ${favorite.favoriteId} for user: ${favorite.userId}',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[FavoritesLocalDataSource] Error adding favorite: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a favorite from the database
|
||||
///
|
||||
/// Removes a favorite by finding it with the combination of [productId] and [userId].
|
||||
/// Returns true if the favorite was found and removed, false otherwise.
|
||||
Future<bool> removeFavorite(String productId, String userId) async {
|
||||
try {
|
||||
// Find the favorite by productId and userId
|
||||
final favorites = _box.values
|
||||
.whereType<FavoriteModel>()
|
||||
.where((fav) => fav.productId == productId && fav.userId == userId)
|
||||
.toList();
|
||||
|
||||
if (favorites.isEmpty) {
|
||||
debugPrint(
|
||||
'[FavoritesLocalDataSource] Favorite not found: productId=$productId, userId=$userId',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
final favorite = favorites.first;
|
||||
await _box.delete(favorite.favoriteId);
|
||||
debugPrint(
|
||||
'[FavoritesLocalDataSource] Removed favorite: ${favorite.favoriteId} for user: $userId',
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('[FavoritesLocalDataSource] Error removing favorite: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a product is favorited by a user
|
||||
///
|
||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
||||
bool isFavorite(String productId, String userId) {
|
||||
try {
|
||||
return _box.values.whereType<FavoriteModel>().any(
|
||||
(fav) => fav.productId == productId && fav.userId == userId,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[FavoritesLocalDataSource] Error checking favorite: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all favorites for a specific user
|
||||
///
|
||||
/// Removes all favorites for the given [userId].
|
||||
/// Useful for logout or data cleanup scenarios.
|
||||
Future<void> clearFavorites(String userId) async {
|
||||
try {
|
||||
final favoriteIds = _box.values
|
||||
.whereType<FavoriteModel>()
|
||||
.where((fav) => fav.userId == userId)
|
||||
.map((fav) => fav.favoriteId)
|
||||
.toList();
|
||||
|
||||
await _box.deleteAll(favoriteIds);
|
||||
debugPrint(
|
||||
'[FavoritesLocalDataSource] Cleared ${favoriteIds.length} favorites for user: $userId',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[FavoritesLocalDataSource] Error clearing favorites: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the count of favorites for a user
|
||||
///
|
||||
/// Returns the total number of favorites for the given [userId].
|
||||
int getFavoriteCount(String userId) {
|
||||
try {
|
||||
return _box.values
|
||||
.whereType<FavoriteModel>()
|
||||
.where((fav) => fav.userId == userId)
|
||||
.length;
|
||||
} catch (e) {
|
||||
debugPrint('[FavoritesLocalDataSource] Error getting favorite count: $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the favorites box is open
|
||||
///
|
||||
/// Returns true if the box is open and ready to use.
|
||||
bool isBoxOpen() {
|
||||
return Hive.isBoxOpen(HiveBoxNames.favoriteBox);
|
||||
}
|
||||
|
||||
/// Compact the favorites box to reduce storage space
|
||||
///
|
||||
/// Should be called periodically to optimize database size.
|
||||
Future<void> compact() async {
|
||||
try {
|
||||
if (isBoxOpen()) {
|
||||
await _box.compact();
|
||||
debugPrint('[FavoritesLocalDataSource] Favorites box compacted');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[FavoritesLocalDataSource] Error compacting favorites box: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug print helper that works in both Flutter and Dart
|
||||
void debugPrint(String message) {
|
||||
print('[FavoritesLocalDataSource] $message');
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:worker/core/constants/api_constants.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/products/data/models/product_model.dart';
|
||||
|
||||
/// Favorites Remote DataSource
|
||||
///
|
||||
/// Handles all API operations for favorites/wishlist using Frappe ERPNext backend.
|
||||
/// Follows the API spec from docs/favorite.sh
|
||||
///
|
||||
/// Note: The API returns Product objects directly, not separate favorite entities.
|
||||
class FavoritesRemoteDataSource {
|
||||
FavoritesRemoteDataSource(this._dio);
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
/// Get all favorites/wishlist items for the current user
|
||||
///
|
||||
/// API: POST /api/method/building_material.building_material.api.item_wishlist.get_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
///
|
||||
/// Response format (from docs/favorite.sh):
|
||||
/// ```json
|
||||
/// {
|
||||
/// "message": [
|
||||
/// {
|
||||
/// "name": "GIB20 G04",
|
||||
/// "item_code": "GIB20 G04",
|
||||
/// "item_name": "Gibellina GIB20 G04",
|
||||
/// "item_group_name": "OUTDOOR [20mm]",
|
||||
/// "custom_link_360": "https://...",
|
||||
/// "thumbnail": "https://...",
|
||||
/// "price": 0,
|
||||
/// "currency": "",
|
||||
/// "conversion_of_sm": 5.5556
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Returns list of Product objects that are favorited
|
||||
Future<List<ProductModel>> getFavorites({
|
||||
int limitStart = 0,
|
||||
int limitPageLength = 0, // 0 means get all
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
'${ApiConstants.frappeApiMethod}${ApiConstants.getFavorites}',
|
||||
data: {
|
||||
'limit_start': limitStart,
|
||||
'limit_page_length': limitPageLength,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.data == null) {
|
||||
throw const ServerException('Response data is null');
|
||||
}
|
||||
|
||||
// Parse response according to Frappe format
|
||||
final data = response.data!;
|
||||
final messageList = data['message'] as List<dynamic>?;
|
||||
|
||||
if (messageList == null || messageList.isEmpty) {
|
||||
_debugPrint('No favorites found');
|
||||
return [];
|
||||
}
|
||||
|
||||
final products = <ProductModel>[];
|
||||
|
||||
// Convert API response - each item is a product object from wishlist
|
||||
for (final item in messageList) {
|
||||
try {
|
||||
final itemMap = item as Map<String, dynamic>;
|
||||
final productModel = ProductModel.fromWishlistApi(itemMap);
|
||||
products.add(productModel);
|
||||
} catch (e) {
|
||||
_debugPrint('Error parsing product: $e');
|
||||
// Continue with other products even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
_debugPrint('Fetched ${products.length} favorite products');
|
||||
return products;
|
||||
} on DioException catch (e) {
|
||||
_handleDioException(e);
|
||||
} catch (e) {
|
||||
_debugPrint('Error getting favorites: $e');
|
||||
throw ServerException('Failed to get favorites: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Add item to wishlist
|
||||
///
|
||||
/// API: POST /api/method/building_material.building_material.api.item_wishlist.add_to_wishlist
|
||||
/// Body: { "item_id": "GIB20 G04" }
|
||||
///
|
||||
/// Returns true if successful
|
||||
Future<bool> addToFavorites(String itemId) async {
|
||||
try {
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
'${ApiConstants.frappeApiMethod}${ApiConstants.addToFavorites}',
|
||||
data: {
|
||||
'item_id': itemId,
|
||||
},
|
||||
);
|
||||
|
||||
_debugPrint('Added to favorites: $itemId');
|
||||
return response.statusCode == 200;
|
||||
} on DioException catch (e) {
|
||||
_handleDioException(e);
|
||||
} catch (e) {
|
||||
_debugPrint('Error adding to favorites: $e');
|
||||
throw ServerException('Failed to add to favorites: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove item from wishlist
|
||||
///
|
||||
/// API: POST /api/method/building_material.building_material.api.item_wishlist.remove_from_wishlist
|
||||
/// Body: { "item_id": "GIB20 G04" }
|
||||
///
|
||||
/// Returns true if successful
|
||||
Future<bool> removeFromFavorites(String itemId) async {
|
||||
try {
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
'${ApiConstants.frappeApiMethod}${ApiConstants.removeFromFavorites}',
|
||||
data: {
|
||||
'item_id': itemId,
|
||||
},
|
||||
);
|
||||
|
||||
_debugPrint('Removed from favorites: $itemId');
|
||||
return response.statusCode == 200;
|
||||
} on DioException catch (e) {
|
||||
_handleDioException(e);
|
||||
} catch (e) {
|
||||
_debugPrint('Error removing from favorites: $e');
|
||||
throw ServerException('Failed to remove from favorites: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ERROR HANDLING
|
||||
// =========================================================================
|
||||
|
||||
/// Handle Dio exceptions and convert to custom exceptions
|
||||
Never _handleDioException(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
throw const NetworkException('Request timeout. Please check your connection.');
|
||||
|
||||
case DioExceptionType.connectionError:
|
||||
throw const NetworkException('No internet connection.');
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = e.response?.statusCode;
|
||||
final message = e.response?.data?['message'] as String? ??
|
||||
e.response?.data?['error'] as String? ??
|
||||
'Server error';
|
||||
|
||||
if (statusCode == 401) {
|
||||
throw const UnauthorizedException('Unauthorized. Please login again.');
|
||||
} else if (statusCode == 403) {
|
||||
throw const UnauthorizedException('Access forbidden.');
|
||||
} else if (statusCode == 404) {
|
||||
throw const ServerException('Resource not found.');
|
||||
} else if (statusCode != null && statusCode >= 500) {
|
||||
throw ServerException('Server error: $message');
|
||||
} else {
|
||||
throw ServerException(message);
|
||||
}
|
||||
|
||||
case DioExceptionType.cancel:
|
||||
throw const NetworkException('Request was cancelled');
|
||||
|
||||
case DioExceptionType.unknown:
|
||||
case DioExceptionType.badCertificate:
|
||||
throw NetworkException('Network error: ${e.message}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DEBUG UTILITIES
|
||||
// ============================================================================
|
||||
|
||||
/// Debug print helper
|
||||
void _debugPrint(String message) {
|
||||
// ignore: avoid_print
|
||||
print('[FavoritesRemoteDataSource] $message');
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/favorites/domain/entities/favorite.dart';
|
||||
|
||||
part 'favorite_model.g.dart';
|
||||
|
||||
/// Favorite Model
|
||||
///
|
||||
/// Hive CE model for storing user's favorite products locally.
|
||||
/// Maps to the 'favorites' table in the database.
|
||||
///
|
||||
/// Type ID: 28
|
||||
@HiveType(typeId: HiveTypeIds.favoriteModel)
|
||||
class FavoriteModel extends HiveObject {
|
||||
FavoriteModel({
|
||||
required this.favoriteId,
|
||||
required this.productId,
|
||||
required this.userId,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
/// Favorite ID (Primary Key)
|
||||
@HiveField(0)
|
||||
final String favoriteId;
|
||||
|
||||
/// Product ID (Foreign Key)
|
||||
@HiveField(1)
|
||||
final String productId;
|
||||
|
||||
/// User ID (Foreign Key)
|
||||
@HiveField(2)
|
||||
final String userId;
|
||||
|
||||
/// Created timestamp
|
||||
@HiveField(3)
|
||||
final DateTime createdAt;
|
||||
|
||||
// =========================================================================
|
||||
// JSON SERIALIZATION
|
||||
// =========================================================================
|
||||
|
||||
/// Create FavoriteModel from JSON
|
||||
factory FavoriteModel.fromJson(Map<String, dynamic> json) {
|
||||
return FavoriteModel(
|
||||
favoriteId: json['favorite_id'] as String,
|
||||
productId: json['product_id'] as String,
|
||||
userId: json['user_id'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert FavoriteModel to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'favorite_id': favoriteId,
|
||||
'product_id': productId,
|
||||
'user_id': userId,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// COPY WITH
|
||||
// =========================================================================
|
||||
|
||||
/// Create a copy with updated fields
|
||||
FavoriteModel copyWith({
|
||||
String? favoriteId,
|
||||
String? productId,
|
||||
String? userId,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return FavoriteModel(
|
||||
favoriteId: favoriteId ?? this.favoriteId,
|
||||
productId: productId ?? this.productId,
|
||||
userId: userId ?? this.userId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FavoriteModel(favoriteId: $favoriteId, productId: $productId, userId: $userId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is FavoriteModel && other.favoriteId == favoriteId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => favoriteId.hashCode;
|
||||
|
||||
// =========================================================================
|
||||
// ENTITY CONVERSION
|
||||
// =========================================================================
|
||||
|
||||
/// Convert FavoriteModel to Favorite entity
|
||||
Favorite toEntity() {
|
||||
return Favorite(
|
||||
favoriteId: favoriteId,
|
||||
productId: productId,
|
||||
userId: userId,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create FavoriteModel from Favorite entity
|
||||
factory FavoriteModel.fromEntity(Favorite favorite) {
|
||||
return FavoriteModel(
|
||||
favoriteId: favorite.favoriteId,
|
||||
productId: favorite.productId,
|
||||
userId: favorite.userId,
|
||||
createdAt: favorite.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'favorite_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class FavoriteModelAdapter extends TypeAdapter<FavoriteModel> {
|
||||
@override
|
||||
final typeId = 28;
|
||||
|
||||
@override
|
||||
FavoriteModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return FavoriteModel(
|
||||
favoriteId: fields[0] as String,
|
||||
productId: fields[1] as String,
|
||||
userId: fields[2] as String,
|
||||
createdAt: fields[3] as DateTime,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, FavoriteModel obj) {
|
||||
writer
|
||||
..writeByte(4)
|
||||
..writeByte(0)
|
||||
..write(obj.favoriteId)
|
||||
..writeByte(1)
|
||||
..write(obj.productId)
|
||||
..writeByte(2)
|
||||
..write(obj.userId)
|
||||
..writeByte(3)
|
||||
..write(obj.createdAt);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is FavoriteModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/core/network/network_info.dart';
|
||||
import 'package:worker/features/favorites/data/datasources/favorite_products_local_datasource.dart';
|
||||
import 'package:worker/features/favorites/data/datasources/favorites_remote_datasource.dart';
|
||||
import 'package:worker/features/favorites/domain/repositories/favorites_repository.dart';
|
||||
import 'package:worker/features/products/domain/entities/product.dart';
|
||||
|
||||
/// Favorites Repository Implementation
|
||||
///
|
||||
/// Implements the FavoritesRepository interface with online-first approach:
|
||||
/// 1. Always try API first when online
|
||||
/// 2. Cache API responses locally
|
||||
/// 3. Fall back to local cache on network errors
|
||||
class FavoritesRepositoryImpl implements FavoritesRepository {
|
||||
FavoritesRepositoryImpl({
|
||||
required this.remoteDataSource,
|
||||
required this.productsLocalDataSource,
|
||||
required this.networkInfo,
|
||||
});
|
||||
|
||||
final FavoritesRemoteDataSource remoteDataSource;
|
||||
final FavoriteProductsLocalDataSource productsLocalDataSource;
|
||||
final NetworkInfo networkInfo;
|
||||
|
||||
// =========================================================================
|
||||
// GET FAVORITE PRODUCTS (Returns actual Product entities)
|
||||
// =========================================================================
|
||||
|
||||
/// Get favorite products with full product data
|
||||
///
|
||||
/// Online-first: Fetches from API, caches locally
|
||||
/// Offline: Returns cached products
|
||||
@override
|
||||
Future<List<Product>> getFavoriteProducts() async {
|
||||
try {
|
||||
// Online-first: Try to fetch from API
|
||||
if (await networkInfo.isConnected) {
|
||||
_debugPrint('Fetching favorite products from API');
|
||||
|
||||
try {
|
||||
// Get products from wishlist API
|
||||
final remoteProducts = await remoteDataSource.getFavorites();
|
||||
|
||||
// Cache products locally
|
||||
await productsLocalDataSource.saveProducts(remoteProducts);
|
||||
|
||||
_debugPrint('Fetched ${remoteProducts.length} favorite products from API');
|
||||
return remoteProducts.map((model) => model.toEntity()).toList();
|
||||
} on ServerException catch (e) {
|
||||
_debugPrint('API error, falling back to cache: $e');
|
||||
return _getProductsFromCache();
|
||||
} on NetworkException catch (e) {
|
||||
_debugPrint('Network error, falling back to cache: $e');
|
||||
return _getProductsFromCache();
|
||||
}
|
||||
} else {
|
||||
// Offline: Use local cache
|
||||
_debugPrint('Offline - using cached favorite products');
|
||||
return _getProductsFromCache();
|
||||
}
|
||||
} catch (e) {
|
||||
_debugPrint('Error getting favorite products: $e');
|
||||
return _getProductsFromCache();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get favorite products from local cache
|
||||
Future<List<Product>> _getProductsFromCache() async {
|
||||
try {
|
||||
final cachedProducts = await productsLocalDataSource.getAllProducts();
|
||||
_debugPrint('Loaded ${cachedProducts.length} favorite products from cache');
|
||||
return cachedProducts.map((model) => model.toEntity()).toList();
|
||||
} catch (e) {
|
||||
_debugPrint('Error loading from cache: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ADD FAVORITE
|
||||
// =========================================================================
|
||||
|
||||
@override
|
||||
Future<void> addFavorite(String productId) async {
|
||||
try {
|
||||
// Online-first: Try to add via API
|
||||
if (await networkInfo.isConnected) {
|
||||
_debugPrint('Adding favorite via API: $productId');
|
||||
|
||||
try {
|
||||
final success = await remoteDataSource.addToFavorites(productId);
|
||||
|
||||
if (success) {
|
||||
_debugPrint('Added favorite successfully: $productId');
|
||||
} else {
|
||||
throw const ServerException('Failed to add to favorites');
|
||||
}
|
||||
} on ServerException catch (e) {
|
||||
_debugPrint('API error adding favorite: $e');
|
||||
rethrow;
|
||||
} on NetworkException catch (e) {
|
||||
_debugPrint('Network error adding favorite: $e');
|
||||
rethrow;
|
||||
}
|
||||
} else {
|
||||
// Offline: Queue for later sync
|
||||
_debugPrint('Offline - cannot add favorite: $productId');
|
||||
throw const NetworkException('No internet connection');
|
||||
}
|
||||
} catch (e) {
|
||||
_debugPrint('Error adding favorite: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// REMOVE FAVORITE
|
||||
// =========================================================================
|
||||
|
||||
@override
|
||||
Future<bool> removeFavorite(String productId) async {
|
||||
try {
|
||||
// Online-first: Try to remove via API
|
||||
if (await networkInfo.isConnected) {
|
||||
_debugPrint('Removing favorite via API: $productId');
|
||||
|
||||
try {
|
||||
final success = await remoteDataSource.removeFromFavorites(productId);
|
||||
|
||||
if (success) {
|
||||
_debugPrint('Removed favorite successfully: $productId');
|
||||
}
|
||||
|
||||
return success;
|
||||
} on ServerException catch (e) {
|
||||
_debugPrint('API error removing favorite: $e');
|
||||
return false;
|
||||
} on NetworkException catch (e) {
|
||||
_debugPrint('Network error removing favorite: $e');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Offline: Cannot remove
|
||||
_debugPrint('Offline - cannot remove favorite: $productId');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
_debugPrint('Error removing favorite: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DEBUG UTILITIES
|
||||
// ============================================================================
|
||||
|
||||
/// Debug print helper
|
||||
void _debugPrint(String message) {
|
||||
// ignore: avoid_print
|
||||
print('[FavoritesRepository] $message');
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/// Domain Entity: Favorite
|
||||
///
|
||||
/// Pure business entity representing a user's favorite product.
|
||||
/// This entity is framework-independent and contains only business logic.
|
||||
library;
|
||||
|
||||
/// Favorite Entity
|
||||
///
|
||||
/// Represents a product that a user has marked as favorite.
|
||||
/// Used across all layers but originates in the domain layer.
|
||||
class Favorite {
|
||||
/// Unique identifier for the favorite entry
|
||||
final String favoriteId;
|
||||
|
||||
/// Reference to the product that was favorited
|
||||
final String productId;
|
||||
|
||||
/// Reference to the user who favorited the product
|
||||
final String userId;
|
||||
|
||||
/// Timestamp when the product was favorited
|
||||
final DateTime createdAt;
|
||||
|
||||
const Favorite({
|
||||
required this.favoriteId,
|
||||
required this.productId,
|
||||
required this.userId,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
/// Copy with method for creating modified copies
|
||||
Favorite copyWith({
|
||||
String? favoriteId,
|
||||
String? productId,
|
||||
String? userId,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return Favorite(
|
||||
favoriteId: favoriteId ?? this.favoriteId,
|
||||
productId: productId ?? this.productId,
|
||||
userId: userId ?? this.userId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Favorite(favoriteId: $favoriteId, productId: $productId, '
|
||||
'userId: $userId, createdAt: $createdAt)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is Favorite &&
|
||||
other.favoriteId == favoriteId &&
|
||||
other.productId == productId &&
|
||||
other.userId == userId &&
|
||||
other.createdAt == createdAt;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(favoriteId, productId, userId, createdAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:worker/features/products/domain/entities/product.dart';
|
||||
|
||||
/// Favorites Repository Interface
|
||||
///
|
||||
/// Defines the contract for favorites data operations.
|
||||
/// Implementations should follow the online-first approach:
|
||||
/// 1. Try to fetch/update data from API
|
||||
/// 2. Update local cache with API response
|
||||
/// 3. On network failure, fall back to local cache
|
||||
abstract class FavoritesRepository {
|
||||
/// Get favorite products with full product data
|
||||
///
|
||||
/// Online-first: Fetches from wishlist API, caches locally.
|
||||
/// Falls back to local cache on network failure.
|
||||
/// Returns `List<Product>` with complete product information.
|
||||
Future<List<Product>> getFavoriteProducts();
|
||||
|
||||
/// Add a product to favorites
|
||||
///
|
||||
/// Online-first: Adds to API, then caches locally.
|
||||
/// On network failure, queues for later sync.
|
||||
Future<void> addFavorite(String productId);
|
||||
|
||||
/// Remove a product from favorites
|
||||
///
|
||||
/// Online-first: Removes from API, then updates local cache.
|
||||
/// On network failure, queues for later sync.
|
||||
Future<bool> removeFavorite(String productId);
|
||||
}
|
||||
@@ -58,14 +58,15 @@ class FavoritesPage extends ConsumerWidget {
|
||||
);
|
||||
|
||||
if (confirmed == true && context.mounted) {
|
||||
// Clear all favorites
|
||||
await ref.read(favoritesProvider.notifier).clearAll();
|
||||
// TODO: Implement clear all functionality
|
||||
// For now, we would need to remove each product individually
|
||||
// or add a clearAll method to the repository
|
||||
|
||||
// Show snackbar
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Đã xóa tất cả yêu thích'),
|
||||
content: Text('Chức năng này đang được phát triển'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -79,6 +80,9 @@ class FavoritesPage extends ConsumerWidget {
|
||||
final favoriteProductsAsync = ref.watch(favoriteProductsProvider);
|
||||
final favoriteCount = ref.watch(favoriteCountProvider);
|
||||
|
||||
// Track if we've loaded data at least once to prevent empty state flash
|
||||
final hasLoadedOnce = favoriteProductsAsync.hasValue || favoriteProductsAsync.hasError;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
appBar: AppBar(
|
||||
@@ -121,26 +125,148 @@ class FavoritesPage extends ConsumerWidget {
|
||||
body: SafeArea(
|
||||
child: favoriteProductsAsync.when(
|
||||
data: (products) {
|
||||
if (products.isEmpty) {
|
||||
// IMPORTANT: Only show empty state after we've confirmed data loaded
|
||||
// This prevents empty state flash during initial load
|
||||
if (products.isEmpty && hasLoadedOnce) {
|
||||
return const _EmptyState();
|
||||
}
|
||||
|
||||
// If products is empty but we haven't loaded yet, show loading
|
||||
if (products.isEmpty && !hasLoadedOnce) {
|
||||
return const _LoadingState();
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(favoritesProvider);
|
||||
ref.invalidate(favoriteProductsProvider);
|
||||
// Use the new refresh method from AsyncNotifier
|
||||
await ref.read(favoriteProductsProvider.notifier).refresh();
|
||||
},
|
||||
child: _FavoritesGrid(products: products),
|
||||
);
|
||||
},
|
||||
loading: () => const _LoadingState(),
|
||||
error: (error, stackTrace) => _ErrorState(
|
||||
error: error,
|
||||
onRetry: () {
|
||||
ref.invalidate(favoritesProvider);
|
||||
ref.invalidate(favoriteProductsProvider);
|
||||
},
|
||||
),
|
||||
loading: () {
|
||||
// IMPORTANT: Check for previous data first to prevent empty state flash
|
||||
final previousValue = favoriteProductsAsync.hasValue
|
||||
? favoriteProductsAsync.value
|
||||
: null;
|
||||
|
||||
// If we have previous data, show it while loading new data
|
||||
if (previousValue != null && previousValue.isNotEmpty) {
|
||||
return Stack(
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(favoriteProductsProvider.notifier).refresh();
|
||||
},
|
||||
child: _FavoritesGrid(products: previousValue),
|
||||
),
|
||||
const Positioned(
|
||||
top: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text('Đang tải...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Check if we should show skeleton or empty state
|
||||
// Use favoriteCount as a hint - if it's > 0, we likely have data coming
|
||||
if (favoriteCount > 0) {
|
||||
// Show skeleton loading for better UX
|
||||
return const _LoadingState();
|
||||
}
|
||||
|
||||
// No previous data and no favorites - show skeleton briefly
|
||||
return const _LoadingState();
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
// Check if we have previous data to show with error
|
||||
final previousValue = favoriteProductsAsync.hasValue
|
||||
? favoriteProductsAsync.value
|
||||
: null;
|
||||
if (previousValue != null && previousValue.isNotEmpty) {
|
||||
// Show previous data with error message
|
||||
return Stack(
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(favoriteProductsProvider.notifier).refresh();
|
||||
},
|
||||
child: _FavoritesGrid(products: previousValue),
|
||||
),
|
||||
Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Material(
|
||||
color: AppColors.danger,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: AppColors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Không thể tải dữ liệu mới',
|
||||
style: TextStyle(color: AppColors.white),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref.read(favoriteProductsProvider.notifier).refresh();
|
||||
},
|
||||
child: const Text(
|
||||
'Thử lại',
|
||||
style: TextStyle(
|
||||
color: AppColors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
// No previous data, show full error state
|
||||
return _ErrorState(
|
||||
error: error,
|
||||
onRetry: () async {
|
||||
await ref.read(favoriteProductsProvider.notifier).refresh();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -188,7 +314,7 @@ class _EmptyState extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
|
||||
// Subtext
|
||||
Text(
|
||||
const Text(
|
||||
'Thêm sản phẩm vào danh sách yêu thích để xem lại sau',
|
||||
style: TextStyle(fontSize: 14.0, color: AppColors.grey500),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -269,9 +395,9 @@ class _ShimmerCard extends StatelessWidget {
|
||||
// Image placeholder
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.grey100,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(ProductCardSpecs.borderRadius),
|
||||
),
|
||||
),
|
||||
@@ -347,11 +473,11 @@ class _ShimmerCard extends StatelessWidget {
|
||||
///
|
||||
/// Displayed when there's an error loading favorites.
|
||||
class _ErrorState extends StatelessWidget {
|
||||
final Object error;
|
||||
final VoidCallback onRetry;
|
||||
|
||||
const _ErrorState({required this.error, required this.onRetry});
|
||||
|
||||
final Object error;
|
||||
final Future<void> Function() onRetry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
@@ -428,10 +554,10 @@ class _ErrorState extends StatelessWidget {
|
||||
///
|
||||
/// Displays favorite products in a grid layout.
|
||||
class _FavoritesGrid extends StatelessWidget {
|
||||
final List<Product> products;
|
||||
|
||||
const _FavoritesGrid({required this.products});
|
||||
|
||||
final List<Product> products;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
|
||||
@@ -1,148 +1,141 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/favorites/data/datasources/favorites_local_datasource.dart';
|
||||
import 'package:worker/features/favorites/data/models/favorite_model.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/core/network/network_info.dart';
|
||||
import 'package:worker/features/favorites/data/datasources/favorite_products_local_datasource.dart';
|
||||
import 'package:worker/features/favorites/data/datasources/favorites_remote_datasource.dart';
|
||||
import 'package:worker/features/favorites/data/repositories/favorites_repository_impl.dart';
|
||||
import 'package:worker/features/favorites/domain/repositories/favorites_repository.dart';
|
||||
import 'package:worker/features/products/domain/entities/product.dart';
|
||||
import 'package:worker/features/products/domain/usecases/get_products.dart';
|
||||
import 'package:worker/features/products/presentation/providers/products_provider.dart';
|
||||
|
||||
part 'favorites_provider.g.dart';
|
||||
|
||||
// ============================================================================
|
||||
// DATASOURCE PROVIDER
|
||||
// DATASOURCE PROVIDERS
|
||||
// ============================================================================
|
||||
|
||||
/// Provides instance of FavoritesLocalDataSource
|
||||
/// Provides instance of FavoritesRemoteDataSource
|
||||
@riverpod
|
||||
FavoritesLocalDataSource favoritesLocalDataSource(Ref ref) {
|
||||
return FavoritesLocalDataSource();
|
||||
Future<FavoritesRemoteDataSource> favoritesRemoteDataSource(Ref ref) async {
|
||||
final dioClient = await ref.watch(dioClientProvider.future);
|
||||
return FavoritesRemoteDataSource(dioClient.dio);
|
||||
}
|
||||
|
||||
/// Provides instance of FavoriteProductsLocalDataSource
|
||||
@riverpod
|
||||
FavoriteProductsLocalDataSource favoriteProductsLocalDataSource(Ref ref) {
|
||||
return FavoriteProductsLocalDataSource();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CURRENT USER ID PROVIDER
|
||||
// REPOSITORY PROVIDER
|
||||
// ============================================================================
|
||||
|
||||
/// Provides the current logged-in user's ID
|
||||
///
|
||||
/// TODO: Replace with actual auth provider integration
|
||||
/// For now, using hardcoded userId for development
|
||||
/// Provides instance of FavoritesRepository with online-first approach
|
||||
@riverpod
|
||||
String currentUserId(Ref ref) {
|
||||
// TODO: Integrate with actual auth provider when available
|
||||
// Example: return ref.watch(authProvider).user?.id ?? 'user_001';
|
||||
return 'user_001';
|
||||
Future<FavoritesRepository> favoritesRepository(Ref ref) async {
|
||||
final remoteDataSource = await ref.watch(favoritesRemoteDataSourceProvider.future);
|
||||
final productsLocalDataSource = ref.watch(favoriteProductsLocalDataSourceProvider);
|
||||
final networkInfo = ref.watch(networkInfoProvider);
|
||||
|
||||
return FavoritesRepositoryImpl(
|
||||
remoteDataSource: remoteDataSource,
|
||||
productsLocalDataSource: productsLocalDataSource,
|
||||
networkInfo: networkInfo,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN FAVORITES PROVIDER
|
||||
// MAIN FAVORITE PRODUCTS PROVIDER
|
||||
// ============================================================================
|
||||
|
||||
/// Manages the favorites state for the current user
|
||||
/// Manages favorite products with full Product data from wishlist API
|
||||
///
|
||||
/// Uses a Set<String> to store product IDs for efficient lookup.
|
||||
/// Data is persisted to Hive for offline access.
|
||||
@riverpod
|
||||
class Favorites extends _$Favorites {
|
||||
late FavoritesLocalDataSource _dataSource;
|
||||
late String _userId;
|
||||
/// This is the MAIN provider for the favorites feature.
|
||||
/// Returns full Product objects with all data from the wishlist API.
|
||||
///
|
||||
/// Online-first: Fetches from API, caches locally
|
||||
/// Offline: Returns cached products
|
||||
///
|
||||
/// Uses keepAlive to prevent unnecessary reloads.
|
||||
/// Provides refresh() method for pull-to-refresh functionality.
|
||||
///
|
||||
/// AsyncNotifier pattern allows:
|
||||
/// - Manual refresh capability
|
||||
/// - Proper loading states during operations
|
||||
/// - State updates after mutations
|
||||
/// - Better error handling
|
||||
@Riverpod(keepAlive: true)
|
||||
class FavoriteProducts extends _$FavoriteProducts {
|
||||
late FavoritesRepository _repository;
|
||||
|
||||
@override
|
||||
Future<Set<String>> build() async {
|
||||
_dataSource = ref.read(favoritesLocalDataSourceProvider);
|
||||
_userId = ref.read(currentUserIdProvider);
|
||||
|
||||
// Load favorites from Hive
|
||||
return await _loadFavorites();
|
||||
Future<List<Product>> build() async {
|
||||
_repository = await ref.read(favoritesRepositoryProvider.future);
|
||||
return await _loadProducts();
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// PRIVATE METHODS
|
||||
// ==========================================================================
|
||||
|
||||
/// Load favorites from Hive database
|
||||
Future<Set<String>> _loadFavorites() async {
|
||||
/// Load favorite products from repository
|
||||
///
|
||||
/// Online-first: Fetches from API, caches locally
|
||||
/// Falls back to local cache on network failure
|
||||
Future<List<Product>> _loadProducts() async {
|
||||
try {
|
||||
final favorites = await _dataSource.getAllFavorites(_userId);
|
||||
final productIds = favorites.map((fav) => fav.productId).toSet();
|
||||
|
||||
debugPrint('Loaded ${productIds.length} favorites for user: $_userId');
|
||||
return productIds;
|
||||
final products = await _repository.getFavoriteProducts();
|
||||
_debugPrint('Loaded ${products.length} favorite products');
|
||||
return products;
|
||||
} catch (e) {
|
||||
debugPrint('Error loading favorites: $e');
|
||||
return {};
|
||||
_debugPrint('Error loading favorite products: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a unique favorite ID
|
||||
String _generateFavoriteId(String productId) {
|
||||
// Using format: userId_productId_timestamp
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
return '${_userId}_${productId}_$timestamp';
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// PUBLIC METHODS
|
||||
// ==========================================================================
|
||||
|
||||
/// Add a product to favorites
|
||||
///
|
||||
/// Creates a new favorite entry and persists it to Hive.
|
||||
/// If the product is already favorited, this operation is a no-op.
|
||||
/// Calls API to add to wishlist, then refreshes the products list.
|
||||
/// No userId needed - the API uses the authenticated session.
|
||||
Future<void> addFavorite(String productId) async {
|
||||
try {
|
||||
// Check if already favorited
|
||||
final currentState = state.value ?? <String>{};
|
||||
if (currentState.contains(productId)) {
|
||||
debugPrint('Product $productId is already favorited');
|
||||
return;
|
||||
}
|
||||
_debugPrint('Adding product to favorites: $productId');
|
||||
|
||||
// Create favorite model
|
||||
final favorite = FavoriteModel(
|
||||
favoriteId: _generateFavoriteId(productId),
|
||||
productId: productId,
|
||||
userId: _userId,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
// Call repository to add to favorites (uses auth token from session)
|
||||
await _repository.addFavorite(productId);
|
||||
|
||||
// Persist to Hive
|
||||
await _dataSource.addFavorite(favorite);
|
||||
// Refresh the products list after successful addition
|
||||
await refresh();
|
||||
|
||||
// Update state
|
||||
final newState = <String>{...currentState, productId};
|
||||
state = AsyncValue.data(newState);
|
||||
|
||||
debugPrint('Added favorite: $productId');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Error adding favorite: $e');
|
||||
state = AsyncValue.error(e, stackTrace);
|
||||
_debugPrint('Successfully added favorite: $productId');
|
||||
} catch (e) {
|
||||
_debugPrint('Error adding favorite: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a product from favorites
|
||||
///
|
||||
/// Removes the favorite entry from Hive.
|
||||
/// If the product is not favorited, this operation is a no-op.
|
||||
/// Calls API to remove from wishlist, then refreshes the products list.
|
||||
/// No userId needed - the API uses the authenticated session.
|
||||
Future<void> removeFavorite(String productId) async {
|
||||
try {
|
||||
// Check if favorited
|
||||
final currentState = state.value ?? <String>{};
|
||||
if (!currentState.contains(productId)) {
|
||||
debugPrint('Product $productId is not favorited');
|
||||
return;
|
||||
}
|
||||
_debugPrint('Removing product from favorites: $productId');
|
||||
|
||||
// Remove from Hive
|
||||
await _dataSource.removeFavorite(productId, _userId);
|
||||
// Call repository to remove from favorites (uses auth token from session)
|
||||
await _repository.removeFavorite(productId);
|
||||
|
||||
// Update state
|
||||
final newState = <String>{...currentState};
|
||||
newState.remove(productId);
|
||||
state = AsyncValue.data(newState);
|
||||
// Refresh the products list after successful removal
|
||||
await refresh();
|
||||
|
||||
debugPrint('Removed favorite: $productId');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Error removing favorite: $e');
|
||||
state = AsyncValue.error(e, stackTrace);
|
||||
_debugPrint('Successfully removed favorite: $productId');
|
||||
} catch (e) {
|
||||
_debugPrint('Error removing favorite: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,38 +144,26 @@ class Favorites extends _$Favorites {
|
||||
/// If the product is favorited, it will be removed.
|
||||
/// If the product is not favorited, it will be added.
|
||||
Future<void> toggleFavorite(String productId) async {
|
||||
final currentState = state.value ?? <String>{};
|
||||
final currentProducts = state.value ?? [];
|
||||
final isFavorited = currentProducts.any((p) => p.productId == productId);
|
||||
|
||||
if (currentState.contains(productId)) {
|
||||
if (isFavorited) {
|
||||
await removeFavorite(productId);
|
||||
} else {
|
||||
await addFavorite(productId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh favorites from database
|
||||
/// Refresh favorite products from API
|
||||
///
|
||||
/// Useful for syncing state after external changes or on app resume.
|
||||
/// Used for pull-to-refresh functionality.
|
||||
/// Fetches latest data from API and updates cache.
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
return await _loadFavorites();
|
||||
return await _loadProducts();
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear all favorites for the current user
|
||||
///
|
||||
/// Removes all favorite entries from Hive.
|
||||
Future<void> clearAll() async {
|
||||
try {
|
||||
await _dataSource.clearFavorites(_userId);
|
||||
state = const AsyncValue.data({});
|
||||
debugPrint('Cleared all favorites for user: $_userId');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Error clearing favorites: $e');
|
||||
state = AsyncValue.error(e, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -191,14 +172,15 @@ class Favorites extends _$Favorites {
|
||||
|
||||
/// Check if a specific product is favorited
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
||||
/// Safe to use in build methods - will return false during loading/error states.
|
||||
@riverpod
|
||||
bool isFavorite(Ref ref, String productId) {
|
||||
final favoritesAsync = ref.watch(favoritesProvider);
|
||||
final favoriteProductsAsync = ref.watch(favoriteProductsProvider);
|
||||
|
||||
return favoritesAsync.when(
|
||||
data: (favorites) => favorites.contains(productId),
|
||||
return favoriteProductsAsync.when(
|
||||
data: (products) => products.any((p) => p.productId == productId),
|
||||
loading: () => false,
|
||||
error: (_, __) => false,
|
||||
);
|
||||
@@ -206,14 +188,15 @@ bool isFavorite(Ref ref, String productId) {
|
||||
|
||||
/// Get the total count of favorites
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Returns the number of products in the user's favorites.
|
||||
/// Safe to use in build methods - will return 0 during loading/error states.
|
||||
@riverpod
|
||||
int favoriteCount(Ref ref) {
|
||||
final favoritesAsync = ref.watch(favoritesProvider);
|
||||
final favoriteProductsAsync = ref.watch(favoriteProductsProvider);
|
||||
|
||||
return favoritesAsync.when(
|
||||
data: (favorites) => favorites.length,
|
||||
return favoriteProductsAsync.when(
|
||||
data: (products) => products.length,
|
||||
loading: () => 0,
|
||||
error: (_, __) => 0,
|
||||
);
|
||||
@@ -221,51 +204,26 @@ int favoriteCount(Ref ref) {
|
||||
|
||||
/// Get all favorite product IDs as a list
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Useful for filtering product lists or bulk operations.
|
||||
/// Returns an empty list during loading/error states.
|
||||
@riverpod
|
||||
List<String> favoriteProductIds(Ref ref) {
|
||||
final favoritesAsync = ref.watch(favoritesProvider);
|
||||
final favoriteProductsAsync = ref.watch(favoriteProductsProvider);
|
||||
|
||||
return favoritesAsync.when(
|
||||
data: (favorites) => favorites.toList(),
|
||||
return favoriteProductsAsync.when(
|
||||
data: (products) => products.map((p) => p.productId).toList(),
|
||||
loading: () => <String>[],
|
||||
error: (_, __) => <String>[],
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FAVORITE PRODUCTS PROVIDER
|
||||
// ============================================================================
|
||||
|
||||
/// Get actual Product entities for favorited product IDs
|
||||
///
|
||||
/// Combines favorites state with products data to return full Product objects.
|
||||
/// This is useful for displaying favorite products with complete information.
|
||||
@riverpod
|
||||
Future<List<Product>> favoriteProducts(Ref ref) async {
|
||||
final favoriteIds = ref.watch(favoriteProductIdsProvider);
|
||||
|
||||
if (favoriteIds.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get products repository with injected dependencies
|
||||
final productsRepository = await ref.watch(productsRepositoryProvider.future);
|
||||
final getProductsUseCase = GetProducts(productsRepository);
|
||||
final allProducts = await getProductsUseCase();
|
||||
|
||||
// Filter to only include favorited products
|
||||
return allProducts
|
||||
.where((product) => favoriteIds.contains(product.productId))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DEBUG UTILITIES
|
||||
// ============================================================================
|
||||
|
||||
/// Debug print helper
|
||||
void debugPrint(String message) {
|
||||
void _debugPrint(String message) {
|
||||
// ignore: avoid_print
|
||||
print('[FavoritesProvider] $message');
|
||||
}
|
||||
|
||||
@@ -8,170 +8,260 @@ part of 'favorites_provider.dart';
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provides instance of FavoritesLocalDataSource
|
||||
/// Provides instance of FavoritesRemoteDataSource
|
||||
|
||||
@ProviderFor(favoritesLocalDataSource)
|
||||
const favoritesLocalDataSourceProvider = FavoritesLocalDataSourceProvider._();
|
||||
@ProviderFor(favoritesRemoteDataSource)
|
||||
const favoritesRemoteDataSourceProvider = FavoritesRemoteDataSourceProvider._();
|
||||
|
||||
/// Provides instance of FavoritesLocalDataSource
|
||||
/// Provides instance of FavoritesRemoteDataSource
|
||||
|
||||
final class FavoritesLocalDataSourceProvider
|
||||
final class FavoritesRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
FavoritesLocalDataSource,
|
||||
FavoritesLocalDataSource,
|
||||
FavoritesLocalDataSource
|
||||
AsyncValue<FavoritesRemoteDataSource>,
|
||||
FavoritesRemoteDataSource,
|
||||
FutureOr<FavoritesRemoteDataSource>
|
||||
>
|
||||
with $Provider<FavoritesLocalDataSource> {
|
||||
/// Provides instance of FavoritesLocalDataSource
|
||||
const FavoritesLocalDataSourceProvider._()
|
||||
with
|
||||
$FutureModifier<FavoritesRemoteDataSource>,
|
||||
$FutureProvider<FavoritesRemoteDataSource> {
|
||||
/// Provides instance of FavoritesRemoteDataSource
|
||||
const FavoritesRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'favoritesLocalDataSourceProvider',
|
||||
name: r'favoritesRemoteDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$favoritesLocalDataSourceHash();
|
||||
String debugGetCreateSourceHash() => _$favoritesRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<FavoritesLocalDataSource> $createElement(
|
||||
$FutureProviderElement<FavoritesRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<FavoritesRemoteDataSource> create(Ref ref) {
|
||||
return favoritesRemoteDataSource(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$favoritesRemoteDataSourceHash() =>
|
||||
r'ec129162e49f37512950106516c0be6cbe1dfceb';
|
||||
|
||||
/// Provides instance of FavoriteProductsLocalDataSource
|
||||
|
||||
@ProviderFor(favoriteProductsLocalDataSource)
|
||||
const favoriteProductsLocalDataSourceProvider =
|
||||
FavoriteProductsLocalDataSourceProvider._();
|
||||
|
||||
/// Provides instance of FavoriteProductsLocalDataSource
|
||||
|
||||
final class FavoriteProductsLocalDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
FavoriteProductsLocalDataSource,
|
||||
FavoriteProductsLocalDataSource,
|
||||
FavoriteProductsLocalDataSource
|
||||
>
|
||||
with $Provider<FavoriteProductsLocalDataSource> {
|
||||
/// Provides instance of FavoriteProductsLocalDataSource
|
||||
const FavoriteProductsLocalDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'favoriteProductsLocalDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$favoriteProductsLocalDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<FavoriteProductsLocalDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FavoritesLocalDataSource create(Ref ref) {
|
||||
return favoritesLocalDataSource(ref);
|
||||
FavoriteProductsLocalDataSource create(Ref ref) {
|
||||
return favoriteProductsLocalDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(FavoritesLocalDataSource value) {
|
||||
Override overrideWithValue(FavoriteProductsLocalDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<FavoritesLocalDataSource>(value),
|
||||
providerOverride: $SyncValueProvider<FavoriteProductsLocalDataSource>(
|
||||
value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$favoritesLocalDataSourceHash() =>
|
||||
r'2f6ff99042b7cc1087d8cfdad517f448952c25be';
|
||||
String _$favoriteProductsLocalDataSourceHash() =>
|
||||
r'852ae8132f466b3fa6549c26880821ea31e00092';
|
||||
|
||||
/// Provides the current logged-in user's ID
|
||||
///
|
||||
/// TODO: Replace with actual auth provider integration
|
||||
/// For now, using hardcoded userId for development
|
||||
/// Provides instance of FavoritesRepository with online-first approach
|
||||
|
||||
@ProviderFor(currentUserId)
|
||||
const currentUserIdProvider = CurrentUserIdProvider._();
|
||||
@ProviderFor(favoritesRepository)
|
||||
const favoritesRepositoryProvider = FavoritesRepositoryProvider._();
|
||||
|
||||
/// Provides the current logged-in user's ID
|
||||
///
|
||||
/// TODO: Replace with actual auth provider integration
|
||||
/// For now, using hardcoded userId for development
|
||||
/// Provides instance of FavoritesRepository with online-first approach
|
||||
|
||||
final class CurrentUserIdProvider
|
||||
extends $FunctionalProvider<String, String, String>
|
||||
with $Provider<String> {
|
||||
/// Provides the current logged-in user's ID
|
||||
///
|
||||
/// TODO: Replace with actual auth provider integration
|
||||
/// For now, using hardcoded userId for development
|
||||
const CurrentUserIdProvider._()
|
||||
final class FavoritesRepositoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<FavoritesRepository>,
|
||||
FavoritesRepository,
|
||||
FutureOr<FavoritesRepository>
|
||||
>
|
||||
with
|
||||
$FutureModifier<FavoritesRepository>,
|
||||
$FutureProvider<FavoritesRepository> {
|
||||
/// Provides instance of FavoritesRepository with online-first approach
|
||||
const FavoritesRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'currentUserIdProvider',
|
||||
name: r'favoritesRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$currentUserIdHash();
|
||||
String debugGetCreateSourceHash() => _$favoritesRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<String> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
$FutureProviderElement<FavoritesRepository> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
String create(Ref ref) {
|
||||
return currentUserId(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String>(value),
|
||||
);
|
||||
FutureOr<FavoritesRepository> create(Ref ref) {
|
||||
return favoritesRepository(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$currentUserIdHash() => r'7f968e463454a4ad87bce0442f62ecc24a6f756e';
|
||||
String _$favoritesRepositoryHash() =>
|
||||
r'1856b5972aaf9d243f8e5450973ea3ab4aead3f6';
|
||||
|
||||
/// Manages the favorites state for the current user
|
||||
/// Manages favorite products with full Product data from wishlist API
|
||||
///
|
||||
/// Uses a Set<String> to store product IDs for efficient lookup.
|
||||
/// Data is persisted to Hive for offline access.
|
||||
|
||||
@ProviderFor(Favorites)
|
||||
const favoritesProvider = FavoritesProvider._();
|
||||
|
||||
/// Manages the favorites state for the current user
|
||||
/// This is the MAIN provider for the favorites feature.
|
||||
/// Returns full Product objects with all data from the wishlist API.
|
||||
///
|
||||
/// Uses a Set<String> to store product IDs for efficient lookup.
|
||||
/// Data is persisted to Hive for offline access.
|
||||
final class FavoritesProvider
|
||||
extends $AsyncNotifierProvider<Favorites, Set<String>> {
|
||||
/// Manages the favorites state for the current user
|
||||
/// Online-first: Fetches from API, caches locally
|
||||
/// Offline: Returns cached products
|
||||
///
|
||||
/// Uses keepAlive to prevent unnecessary reloads.
|
||||
/// Provides refresh() method for pull-to-refresh functionality.
|
||||
///
|
||||
/// AsyncNotifier pattern allows:
|
||||
/// - Manual refresh capability
|
||||
/// - Proper loading states during operations
|
||||
/// - State updates after mutations
|
||||
/// - Better error handling
|
||||
|
||||
@ProviderFor(FavoriteProducts)
|
||||
const favoriteProductsProvider = FavoriteProductsProvider._();
|
||||
|
||||
/// Manages favorite products with full Product data from wishlist API
|
||||
///
|
||||
/// This is the MAIN provider for the favorites feature.
|
||||
/// Returns full Product objects with all data from the wishlist API.
|
||||
///
|
||||
/// Online-first: Fetches from API, caches locally
|
||||
/// Offline: Returns cached products
|
||||
///
|
||||
/// Uses keepAlive to prevent unnecessary reloads.
|
||||
/// Provides refresh() method for pull-to-refresh functionality.
|
||||
///
|
||||
/// AsyncNotifier pattern allows:
|
||||
/// - Manual refresh capability
|
||||
/// - Proper loading states during operations
|
||||
/// - State updates after mutations
|
||||
/// - Better error handling
|
||||
final class FavoriteProductsProvider
|
||||
extends $AsyncNotifierProvider<FavoriteProducts, List<Product>> {
|
||||
/// Manages favorite products with full Product data from wishlist API
|
||||
///
|
||||
/// Uses a Set<String> to store product IDs for efficient lookup.
|
||||
/// Data is persisted to Hive for offline access.
|
||||
const FavoritesProvider._()
|
||||
/// This is the MAIN provider for the favorites feature.
|
||||
/// Returns full Product objects with all data from the wishlist API.
|
||||
///
|
||||
/// Online-first: Fetches from API, caches locally
|
||||
/// Offline: Returns cached products
|
||||
///
|
||||
/// Uses keepAlive to prevent unnecessary reloads.
|
||||
/// Provides refresh() method for pull-to-refresh functionality.
|
||||
///
|
||||
/// AsyncNotifier pattern allows:
|
||||
/// - Manual refresh capability
|
||||
/// - Proper loading states during operations
|
||||
/// - State updates after mutations
|
||||
/// - Better error handling
|
||||
const FavoriteProductsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'favoritesProvider',
|
||||
isAutoDispose: true,
|
||||
name: r'favoriteProductsProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$favoritesHash();
|
||||
String debugGetCreateSourceHash() => _$favoriteProductsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Favorites create() => Favorites();
|
||||
FavoriteProducts create() => FavoriteProducts();
|
||||
}
|
||||
|
||||
String _$favoritesHash() => r'fccd46f5cd1bbf2b58a13ea90c6d1644ece767b0';
|
||||
String _$favoriteProductsHash() => r'd43c41db210259021df104f9fecdd00cf474d196';
|
||||
|
||||
/// Manages the favorites state for the current user
|
||||
/// Manages favorite products with full Product data from wishlist API
|
||||
///
|
||||
/// Uses a Set<String> to store product IDs for efficient lookup.
|
||||
/// Data is persisted to Hive for offline access.
|
||||
/// This is the MAIN provider for the favorites feature.
|
||||
/// Returns full Product objects with all data from the wishlist API.
|
||||
///
|
||||
/// Online-first: Fetches from API, caches locally
|
||||
/// Offline: Returns cached products
|
||||
///
|
||||
/// Uses keepAlive to prevent unnecessary reloads.
|
||||
/// Provides refresh() method for pull-to-refresh functionality.
|
||||
///
|
||||
/// AsyncNotifier pattern allows:
|
||||
/// - Manual refresh capability
|
||||
/// - Proper loading states during operations
|
||||
/// - State updates after mutations
|
||||
/// - Better error handling
|
||||
|
||||
abstract class _$Favorites extends $AsyncNotifier<Set<String>> {
|
||||
FutureOr<Set<String>> build();
|
||||
abstract class _$FavoriteProducts extends $AsyncNotifier<List<Product>> {
|
||||
FutureOr<List<Product>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<Set<String>>, Set<String>>;
|
||||
final ref = this.ref as $Ref<AsyncValue<List<Product>>, List<Product>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<Set<String>>, Set<String>>,
|
||||
AsyncValue<Set<String>>,
|
||||
AnyNotifier<AsyncValue<List<Product>>, List<Product>>,
|
||||
AsyncValue<List<Product>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
@@ -181,6 +271,7 @@ abstract class _$Favorites extends $AsyncNotifier<Set<String>> {
|
||||
|
||||
/// Check if a specific product is favorited
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
||||
/// Safe to use in build methods - will return false during loading/error states.
|
||||
|
||||
@@ -189,6 +280,7 @@ const isFavoriteProvider = IsFavoriteFamily._();
|
||||
|
||||
/// Check if a specific product is favorited
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
||||
/// Safe to use in build methods - will return false during loading/error states.
|
||||
|
||||
@@ -196,6 +288,7 @@ final class IsFavoriteProvider extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// Check if a specific product is favorited
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
||||
/// Safe to use in build methods - will return false during loading/error states.
|
||||
const IsFavoriteProvider._({
|
||||
@@ -249,10 +342,11 @@ final class IsFavoriteProvider extends $FunctionalProvider<bool, bool, bool>
|
||||
}
|
||||
}
|
||||
|
||||
String _$isFavoriteHash() => r'8d69e5efe981a3717eebdd7ee192fd75afe722d5';
|
||||
String _$isFavoriteHash() => r'6e2f5a50d2350975e17d91f395595cd284b69c20';
|
||||
|
||||
/// Check if a specific product is favorited
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
||||
/// Safe to use in build methods - will return false during loading/error states.
|
||||
|
||||
@@ -269,6 +363,7 @@ final class IsFavoriteFamily extends $Family
|
||||
|
||||
/// Check if a specific product is favorited
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Returns true if the product is in the user's favorites, false otherwise.
|
||||
/// Safe to use in build methods - will return false during loading/error states.
|
||||
|
||||
@@ -281,6 +376,7 @@ final class IsFavoriteFamily extends $Family
|
||||
|
||||
/// Get the total count of favorites
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Returns the number of products in the user's favorites.
|
||||
/// Safe to use in build methods - will return 0 during loading/error states.
|
||||
|
||||
@@ -289,6 +385,7 @@ const favoriteCountProvider = FavoriteCountProvider._();
|
||||
|
||||
/// Get the total count of favorites
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Returns the number of products in the user's favorites.
|
||||
/// Safe to use in build methods - will return 0 during loading/error states.
|
||||
|
||||
@@ -296,6 +393,7 @@ final class FavoriteCountProvider extends $FunctionalProvider<int, int, int>
|
||||
with $Provider<int> {
|
||||
/// Get the total count of favorites
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Returns the number of products in the user's favorites.
|
||||
/// Safe to use in build methods - will return 0 during loading/error states.
|
||||
const FavoriteCountProvider._()
|
||||
@@ -331,10 +429,11 @@ final class FavoriteCountProvider extends $FunctionalProvider<int, int, int>
|
||||
}
|
||||
}
|
||||
|
||||
String _$favoriteCountHash() => r'1f147fe5ef28b1477034bd567cfc05ab3e8e90db';
|
||||
String _$favoriteCountHash() => r'f6f9ab69653671dbc6085dc75b2cae35a47c31a5';
|
||||
|
||||
/// Get all favorite product IDs as a list
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Useful for filtering product lists or bulk operations.
|
||||
/// Returns an empty list during loading/error states.
|
||||
|
||||
@@ -343,6 +442,7 @@ const favoriteProductIdsProvider = FavoriteProductIdsProvider._();
|
||||
|
||||
/// Get all favorite product IDs as a list
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Useful for filtering product lists or bulk operations.
|
||||
/// Returns an empty list during loading/error states.
|
||||
|
||||
@@ -351,6 +451,7 @@ final class FavoriteProductIdsProvider
|
||||
with $Provider<List<String>> {
|
||||
/// Get all favorite product IDs as a list
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
/// Useful for filtering product lists or bulk operations.
|
||||
/// Returns an empty list during loading/error states.
|
||||
const FavoriteProductIdsProvider._()
|
||||
@@ -387,57 +488,4 @@ final class FavoriteProductIdsProvider
|
||||
}
|
||||
|
||||
String _$favoriteProductIdsHash() =>
|
||||
r'a6814af9a1775b908b4101e64ce3056e1534b561';
|
||||
|
||||
/// Get actual Product entities for favorited product IDs
|
||||
///
|
||||
/// Combines favorites state with products data to return full Product objects.
|
||||
/// This is useful for displaying favorite products with complete information.
|
||||
|
||||
@ProviderFor(favoriteProducts)
|
||||
const favoriteProductsProvider = FavoriteProductsProvider._();
|
||||
|
||||
/// Get actual Product entities for favorited product IDs
|
||||
///
|
||||
/// Combines favorites state with products data to return full Product objects.
|
||||
/// This is useful for displaying favorite products with complete information.
|
||||
|
||||
final class FavoriteProductsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<Product>>,
|
||||
List<Product>,
|
||||
FutureOr<List<Product>>
|
||||
>
|
||||
with $FutureModifier<List<Product>>, $FutureProvider<List<Product>> {
|
||||
/// Get actual Product entities for favorited product IDs
|
||||
///
|
||||
/// Combines favorites state with products data to return full Product objects.
|
||||
/// This is useful for displaying favorite products with complete information.
|
||||
const FavoriteProductsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'favoriteProductsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$favoriteProductsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<Product>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<Product>> create(Ref ref) {
|
||||
return favoriteProducts(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$favoriteProductsHash() => r'630acfbc403cc4deb486c7b0199f128252a8990b';
|
||||
r'2e281e9a5dee122d326354afd515a68c7f0c4137';
|
||||
|
||||
@@ -21,9 +21,9 @@ import 'package:worker/features/products/domain/entities/product.dart';
|
||||
/// Displays product information in a card format with a favorite toggle button.
|
||||
/// Used in the favorites grid view.
|
||||
class FavoriteProductCard extends ConsumerWidget {
|
||||
final Product product;
|
||||
|
||||
const FavoriteProductCard({super.key, required this.product});
|
||||
final Product product;
|
||||
|
||||
String _formatPrice(double price) {
|
||||
final formatter = NumberFormat('#,###', 'vi_VN');
|
||||
@@ -59,7 +59,7 @@ class FavoriteProductCard extends ConsumerWidget {
|
||||
if (confirmed == true && context.mounted) {
|
||||
// Remove from favorites
|
||||
await ref
|
||||
.read(favoritesProvider.notifier)
|
||||
.read(favoriteProductsProvider.notifier)
|
||||
.removeFavorite(product.productId);
|
||||
|
||||
// Show snackbar
|
||||
@@ -94,7 +94,7 @@ class FavoriteProductCard extends ConsumerWidget {
|
||||
top: Radius.circular(ProductCardSpecs.borderRadius),
|
||||
),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: product.imageUrl,
|
||||
imageUrl: product.thumbnail,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
|
||||
@@ -292,6 +292,59 @@ class ProductModel extends HiveObject {
|
||||
);
|
||||
}
|
||||
|
||||
/// Create ProductModel from Wishlist API JSON
|
||||
///
|
||||
/// The wishlist API returns a simplified product structure:
|
||||
/// - name: Item code (e.g., "GIB20 G04")
|
||||
/// - item_code: Item code (duplicate of name)
|
||||
/// - item_name: Display name (e.g., "Gibellina GIB20 G04")
|
||||
/// - item_group_name: Category (e.g., "OUTDOOR [20mm]")
|
||||
/// - custom_link_360: 360 view link
|
||||
/// - thumbnail: Thumbnail image URL
|
||||
/// - price: Price (usually 0 from wishlist)
|
||||
/// - currency: Currency code
|
||||
/// - conversion_of_sm: Conversion factor
|
||||
factory ProductModel.fromWishlistApi(Map<String, dynamic> json) {
|
||||
final now = DateTime.now();
|
||||
|
||||
// Handle thumbnail URL
|
||||
String thumbnailUrl = '';
|
||||
if (json['thumbnail'] != null && (json['thumbnail'] as String).isNotEmpty) {
|
||||
final thumbnailPath = json['thumbnail'] as String;
|
||||
if (thumbnailPath.startsWith('http')) {
|
||||
thumbnailUrl = thumbnailPath;
|
||||
} else if (thumbnailPath.startsWith('/')) {
|
||||
thumbnailUrl = '${ApiConstants.baseUrl}$thumbnailPath';
|
||||
} else {
|
||||
thumbnailUrl = '${ApiConstants.baseUrl}/$thumbnailPath';
|
||||
}
|
||||
}
|
||||
|
||||
return ProductModel(
|
||||
productId: json['item_code'] as String? ?? json['name'] as String,
|
||||
name: json['item_name'] as String? ?? json['name'] as String,
|
||||
description: null, // Not provided by wishlist API
|
||||
basePrice: (json['price'] as num?)?.toDouble() ?? 0.0,
|
||||
images: null, // Not provided by wishlist API
|
||||
thumbnail: thumbnailUrl,
|
||||
imageCaptions: null,
|
||||
customLink360: json['custom_link_360'] as String?,
|
||||
specifications: null,
|
||||
category: json['item_group_name'] as String?,
|
||||
brand: null, // Not provided by wishlist API
|
||||
unit: json['currency'] as String? ?? 'm²',
|
||||
conversionOfSm: json['conversion_of_sm'] != null
|
||||
? (json['conversion_of_sm'] as num).toDouble()
|
||||
: null,
|
||||
introAttributes: null,
|
||||
isActive: true, // Assume active if in wishlist
|
||||
isFeatured: false,
|
||||
erpnextItemCode: json['item_code'] as String? ?? json['name'] as String,
|
||||
createdAt: now,
|
||||
updatedAt: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert ProductModel to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
|
||||
@@ -61,7 +61,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
|
||||
void _toggleFavorite() async {
|
||||
// Toggle favorite using favorites provider
|
||||
await ref.read(favoritesProvider.notifier).toggleFavorite(widget.productId);
|
||||
await ref.read(favoriteProductsProvider.notifier).toggleFavorite(widget.productId);
|
||||
|
||||
// Show feedback
|
||||
final isFavorite = ref.read(isFavoriteProvider(widget.productId));
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:worker/features/cart/data/models/cart_item_model.dart';
|
||||
import 'package:worker/features/cart/data/models/cart_model.dart';
|
||||
import 'package:worker/features/chat/data/models/chat_room_model.dart';
|
||||
import 'package:worker/features/chat/data/models/message_model.dart';
|
||||
import 'package:worker/features/favorites/data/models/favorite_model.dart';
|
||||
import 'package:worker/features/home/data/models/member_card_model.dart';
|
||||
import 'package:worker/features/home/data/models/promotion_model.dart';
|
||||
import 'package:worker/features/loyalty/data/models/gift_catalog_model.dart';
|
||||
@@ -50,7 +49,6 @@ extension HiveRegistrar on HiveInterface {
|
||||
registerAdapter(DesignStatusAdapter());
|
||||
registerAdapter(EntrySourceAdapter());
|
||||
registerAdapter(EntryTypeAdapter());
|
||||
registerAdapter(FavoriteModelAdapter());
|
||||
registerAdapter(GiftCatalogModelAdapter());
|
||||
registerAdapter(GiftCategoryAdapter());
|
||||
registerAdapter(GiftStatusAdapter());
|
||||
@@ -106,7 +104,6 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
registerAdapter(DesignStatusAdapter());
|
||||
registerAdapter(EntrySourceAdapter());
|
||||
registerAdapter(EntryTypeAdapter());
|
||||
registerAdapter(FavoriteModelAdapter());
|
||||
registerAdapter(GiftCatalogModelAdapter());
|
||||
registerAdapter(GiftCategoryAdapter());
|
||||
registerAdapter(GiftStatusAdapter());
|
||||
|
||||
Reference in New Issue
Block a user