diff --git a/lib/features/auth/presentation/providers/session_provider.dart b/lib/features/auth/presentation/providers/session_provider.dart index c3c85d9..3a3889b 100644 --- a/lib/features/auth/presentation/providers/session_provider.dart +++ b/lib/features/auth/presentation/providers/session_provider.dart @@ -6,8 +6,8 @@ library; import 'package:curl_logger_dio_interceptor/curl_logger_dio_interceptor.dart'; import 'package:dio/dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:worker/core/constants/api_constants.dart'; import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart'; -import 'package:worker/features/auth/data/models/auth_session_model.dart'; part 'session_provider.g.dart'; @@ -16,7 +16,7 @@ part 'session_provider.g.dart'; Dio dio(Ref ref) { final dio = Dio( BaseOptions( - baseUrl: 'https://land.dbiz.com', + baseUrl: ApiConstants.baseUrl, connectTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(seconds: 30), ), diff --git a/lib/features/auth/presentation/providers/session_provider.g.dart b/lib/features/auth/presentation/providers/session_provider.g.dart index 5b81808..d77aba7 100644 --- a/lib/features/auth/presentation/providers/session_provider.g.dart +++ b/lib/features/auth/presentation/providers/session_provider.g.dart @@ -51,7 +51,7 @@ final class DioProvider extends $FunctionalProvider } } -String _$dioHash() => r'2bc10725a1b646cfaabd88c722e5101c06837c75'; +String _$dioHash() => r'3c682dd2b6f7ca8e39e1c26713a9160c2e69d894'; /// Provider for AuthRemoteDataSource diff --git a/lib/features/news/data/datasources/news_remote_datasource.dart b/lib/features/news/data/datasources/news_remote_datasource.dart index 1c7480c..b4cdb6b 100644 --- a/lib/features/news/data/datasources/news_remote_datasource.dart +++ b/lib/features/news/data/datasources/news_remote_datasource.dart @@ -147,7 +147,6 @@ class NewsRemoteDataSource { 'published_on', 'blogger', 'blog_intro', - 'content', 'meta_image', 'meta_description', 'blog_category', diff --git a/lib/features/news/data/models/blog_post_model.dart b/lib/features/news/data/models/blog_post_model.dart index ffb3d33..df5f90d 100644 --- a/lib/features/news/data/models/blog_post_model.dart +++ b/lib/features/news/data/models/blog_post_model.dart @@ -4,6 +4,7 @@ library; import 'package:json_annotation/json_annotation.dart'; +import 'package:worker/core/constants/api_constants.dart'; import 'package:worker/features/news/domain/entities/news_article.dart'; part 'blog_post_model.g.dart'; @@ -97,11 +98,11 @@ class BlogPostModel { if (metaImage != null && metaImage!.isNotEmpty) { // If meta_image starts with /, prepend the base URL if (metaImage!.startsWith('/')) { - imageUrl = 'https://land.dbiz.com$metaImage'; + imageUrl = '${ApiConstants.baseUrl}$metaImage'; } else if (metaImage!.startsWith('http')) { imageUrl = metaImage!; } else { - imageUrl = 'https://land.dbiz.com/$metaImage'; + imageUrl = '${ApiConstants.baseUrl}/$metaImage'; } } else { imageUrl = 'https://via.placeholder.com/400x300?text=${Uri.encodeComponent(title)}'; diff --git a/lib/features/news/presentation/pages/news_detail_page.dart b/lib/features/news/presentation/pages/news_detail_page.dart index 847dc41..d971461 100644 --- a/lib/features/news/presentation/pages/news_detail_page.dart +++ b/lib/features/news/presentation/pages/news_detail_page.dart @@ -154,33 +154,8 @@ class _NewsDetailPageState extends ConsumerState { ), ), - const SizedBox(height: 16), + const SizedBox(height: 8), - // Excerpt - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFFF8FAFC), - border: const Border( - left: BorderSide(color: AppColors.primaryBlue, width: 4), - ), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(8), - bottomRight: Radius.circular(8), - ), - ), - child: Text( - article.excerpt, - style: const TextStyle( - fontSize: 16, - color: Color(0xFF64748B), - fontStyle: FontStyle.italic, - height: 1.5, - ), - ), - ), - - const SizedBox(height: 24), // Article Body - Render HTML content if (article.content != null && article.content!.isNotEmpty) diff --git a/lib/features/products/data/models/product_model.dart b/lib/features/products/data/models/product_model.dart index 36d2ac7..1061710 100644 --- a/lib/features/products/data/models/product_model.dart +++ b/lib/features/products/data/models/product_model.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:hive_ce/hive.dart'; +import 'package:worker/core/constants/api_constants.dart'; import 'package:worker/core/constants/storage_constants.dart'; import 'package:worker/features/products/domain/entities/product.dart'; @@ -141,61 +142,101 @@ class ProductModel extends HiveObject { /// /// Maps Frappe Item doctype fields to our ProductModel structure. /// Frappe fields: - /// - name: Item code (e.g., "GIB20 G02") + /// - name: Item code (e.g., "CHG S01P") /// - item_name: Display name /// - description: Product description - /// - standard_rate: Price + /// - price: Price (from product detail API) + /// - standard_rate: Price (from product list API) /// - stock_uom: Unit of measurement - /// - image: Image path - /// - thumbnail: Thumbnail image path + /// - thumbnail: Thumbnail image URL + /// - image_list: Array of images with image_url /// - brand: Brand name - /// - item_group: Category/group + /// - item_group_name: Category/group name /// - custom_link_360: Custom 360 view link + /// - attributes: Array of product attributes (Size, Color, Surface, etc.) factory ProductModel.fromFrappeJson(Map json) { - // Handle image - prepend base URL if needed - String? imageUrl; - if (json['image'] != null && (json['image'] as String).isNotEmpty) { - final imagePath = json['image'] as String; - if (imagePath.startsWith('/')) { - imageUrl = 'https://land.dbiz.com$imagePath'; - } else if (imagePath.startsWith('http')) { - imageUrl = imagePath; - } else { - imageUrl = 'https://land.dbiz.com/$imagePath'; - } - } - // Handle thumbnail - prepend base URL if needed String? thumbnailUrl; if (json['thumbnail'] != null && (json['thumbnail'] as String).isNotEmpty) { final thumbnailPath = json['thumbnail'] as String; if (thumbnailPath.startsWith('/')) { - thumbnailUrl = 'https://land.dbiz.com$thumbnailPath'; + thumbnailUrl = '${ApiConstants.baseUrl}$thumbnailPath'; } else if (thumbnailPath.startsWith('http')) { thumbnailUrl = thumbnailPath; } else { - thumbnailUrl = 'https://land.dbiz.com/$thumbnailPath'; + thumbnailUrl = '${ApiConstants.baseUrl}/$thumbnailPath'; } } - // Convert single image to list format - final imagesList = imageUrl != null ? [imageUrl] : []; + // Handle image_list array (from product detail API) + final List imagesList = []; + final Map imageCaptionsMap = {}; + + if (json['image_list'] != null && json['image_list'] is List) { + final imageListData = json['image_list'] as List; + for (final imgData in imageListData) { + if (imgData is Map && imgData['image_url'] != null) { + final imageUrl = imgData['image_url'] as String; + imagesList.add(imageUrl); + + // Store image caption (image_name: A, B, C, etc.) + if (imgData['image_name'] != null) { + imageCaptionsMap[imageUrl] = imgData['image_name'] as String; + } + } + } + } + // Fallback to single image field (from product list API) + else if (json['image'] != null && (json['image'] as String).isNotEmpty) { + final imagePath = json['image'] as String; + String imageUrl; + if (imagePath.startsWith('/')) { + imageUrl = '${ApiConstants.baseUrl}$imagePath'; + } else if (imagePath.startsWith('http')) { + imageUrl = imagePath; + } else { + imageUrl = '${ApiConstants.baseUrl}/$imagePath'; + } + imagesList.add(imageUrl); + } + + // Parse attributes array into specifications map + final Map specificationsMap = {}; + if (json['attributes'] != null && json['attributes'] is List) { + final attributesData = json['attributes'] as List; + for (final attr in attributesData) { + if (attr is Map && + attr['attribute_name'] != null && + attr['attribute_value'] != null) { + specificationsMap[attr['attribute_name'] as String] = + attr['attribute_value'] as String; + } + } + } final now = DateTime.now(); + // Handle price from both product detail (price) and product list (standard_rate) + final price = (json['price'] as num?)?.toDouble() ?? + (json['standard_rate'] as num?)?.toDouble() ?? + 0.0; + return ProductModel( productId: json['name'] as String, // Item code name: json['item_name'] as String? ?? json['name'] as String, description: json['description'] as String?, - basePrice: (json['standard_rate'] as num?)?.toDouble() ?? 0.0, + basePrice: price, images: imagesList.isNotEmpty ? jsonEncode(imagesList) : null, thumbnail: thumbnailUrl, - imageCaptions: null, // Not provided by API - customLink360: json['custom_link_360'] as String?, - specifications: json['specifications'] != null - ? jsonEncode(json['specifications']) + imageCaptions: imageCaptionsMap.isNotEmpty + ? jsonEncode(imageCaptionsMap) : null, - category: json['item_group'] as String?, // Frappe uses item_group + customLink360: json['custom_link_360'] as String?, + specifications: specificationsMap.isNotEmpty + ? jsonEncode(specificationsMap) + : null, + category: json['item_group_name'] as String? ?? + json['item_group'] as String?, // Try item_group_name first, fallback to item_group brand: json['brand'] as String?, unit: json['stock_uom'] as String? ?? 'm²', isActive: (json['disabled'] as int?) == 0, // Frappe uses 'disabled' field diff --git a/lib/features/products/presentation/pages/products_page.dart b/lib/features/products/presentation/pages/products_page.dart index b1d3f76..31b7b3c 100644 --- a/lib/features/products/presentation/pages/products_page.dart +++ b/lib/features/products/presentation/pages/products_page.dart @@ -11,6 +11,7 @@ import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/theme/colors.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; import 'package:worker/features/products/presentation/providers/categories_provider.dart'; +import 'package:worker/features/products/presentation/providers/product_filter_options_provider.dart'; import 'package:worker/features/products/presentation/providers/products_provider.dart'; import 'package:worker/features/products/presentation/widgets/category_filter_chips.dart'; import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart'; @@ -31,11 +32,14 @@ class ProductsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); final categoriesAsync = ref.watch(categoriesProvider); final productsAsync = ref.watch(productsProvider); final cartItemCount = ref.watch(cartItemCountProvider); + // Preload filter options for better UX when opening filter drawer + ref.watch(productFilterOptionsProvider); + return Scaffold( backgroundColor: const Color(0xFFF4F6F8), // Match HTML background endDrawer: const ProductFilterDrawer(), diff --git a/lib/features/products/presentation/providers/product_filter_options_provider.dart b/lib/features/products/presentation/providers/product_filter_options_provider.dart index 6f45166..5751b52 100644 --- a/lib/features/products/presentation/providers/product_filter_options_provider.dart +++ b/lib/features/products/presentation/providers/product_filter_options_provider.dart @@ -90,6 +90,9 @@ class FilterOption { /// 2. Product Brands /// 3. Product Attributes /// +/// Memory footprint: ~5-15 KB (negligible) +/// Cache strategy: Keep alive for session duration +/// /// Usage: /// ```dart /// final filterOptionsAsync = ref.watch(productFilterOptionsProvider); @@ -100,7 +103,7 @@ class FilterOption { /// error: (error, stack) => ErrorWidget(error), /// ); /// ``` -@riverpod +@Riverpod(keepAlive: true) Future productFilterOptions(Ref ref) async { try { // Get remote datasource diff --git a/lib/features/products/presentation/providers/product_filter_options_provider.g.dart b/lib/features/products/presentation/providers/product_filter_options_provider.g.dart index 53495a3..da5424f 100644 --- a/lib/features/products/presentation/providers/product_filter_options_provider.g.dart +++ b/lib/features/products/presentation/providers/product_filter_options_provider.g.dart @@ -15,6 +15,9 @@ part of 'product_filter_options_provider.dart'; /// 2. Product Brands /// 3. Product Attributes /// +/// Memory footprint: ~5-15 KB (negligible) +/// Cache strategy: Keep alive for session duration +/// /// Usage: /// ```dart /// final filterOptionsAsync = ref.watch(productFilterOptionsProvider); @@ -36,6 +39,9 @@ const productFilterOptionsProvider = ProductFilterOptionsProvider._(); /// 2. Product Brands /// 3. Product Attributes /// +/// Memory footprint: ~5-15 KB (negligible) +/// Cache strategy: Keep alive for session duration +/// /// Usage: /// ```dart /// final filterOptionsAsync = ref.watch(productFilterOptionsProvider); @@ -64,6 +70,9 @@ final class ProductFilterOptionsProvider /// 2. Product Brands /// 3. Product Attributes /// + /// Memory footprint: ~5-15 KB (negligible) + /// Cache strategy: Keep alive for session duration + /// /// Usage: /// ```dart /// final filterOptionsAsync = ref.watch(productFilterOptionsProvider); @@ -80,7 +89,7 @@ final class ProductFilterOptionsProvider argument: null, retry: null, name: r'productFilterOptionsProvider', - isAutoDispose: true, + isAutoDispose: false, dependencies: null, $allTransitiveDependencies: null, ); @@ -101,4 +110,4 @@ final class ProductFilterOptionsProvider } String _$productFilterOptionsHash() => - r'394f47113bc2afeea8a0a4548df826900884644b'; + r'253586215f05ca2fd1ccae7922b5925150614af0';