update keep alive filter

This commit is contained in:
Phuoc Nguyen
2025-11-11 15:45:32 +07:00
parent b5afeed534
commit 2f296ad8d3
9 changed files with 96 additions and 64 deletions

View File

@@ -6,8 +6,8 @@ library;
import 'package:curl_logger_dio_interceptor/curl_logger_dio_interceptor.dart'; import 'package:curl_logger_dio_interceptor/curl_logger_dio_interceptor.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:riverpod_annotation/riverpod_annotation.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/datasources/auth_remote_datasource.dart';
import 'package:worker/features/auth/data/models/auth_session_model.dart';
part 'session_provider.g.dart'; part 'session_provider.g.dart';
@@ -16,7 +16,7 @@ part 'session_provider.g.dart';
Dio dio(Ref ref) { Dio dio(Ref ref) {
final dio = Dio( final dio = Dio(
BaseOptions( BaseOptions(
baseUrl: 'https://land.dbiz.com', baseUrl: ApiConstants.baseUrl,
connectTimeout: const Duration(seconds: 30), connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(seconds: 30),
), ),

View File

@@ -51,7 +51,7 @@ final class DioProvider extends $FunctionalProvider<Dio, Dio, Dio>
} }
} }
String _$dioHash() => r'2bc10725a1b646cfaabd88c722e5101c06837c75'; String _$dioHash() => r'3c682dd2b6f7ca8e39e1c26713a9160c2e69d894';
/// Provider for AuthRemoteDataSource /// Provider for AuthRemoteDataSource

View File

@@ -147,7 +147,6 @@ class NewsRemoteDataSource {
'published_on', 'published_on',
'blogger', 'blogger',
'blog_intro', 'blog_intro',
'content',
'meta_image', 'meta_image',
'meta_description', 'meta_description',
'blog_category', 'blog_category',

View File

@@ -4,6 +4,7 @@
library; library;
import 'package:json_annotation/json_annotation.dart'; 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'; import 'package:worker/features/news/domain/entities/news_article.dart';
part 'blog_post_model.g.dart'; part 'blog_post_model.g.dart';
@@ -97,11 +98,11 @@ class BlogPostModel {
if (metaImage != null && metaImage!.isNotEmpty) { if (metaImage != null && metaImage!.isNotEmpty) {
// If meta_image starts with /, prepend the base URL // If meta_image starts with /, prepend the base URL
if (metaImage!.startsWith('/')) { if (metaImage!.startsWith('/')) {
imageUrl = 'https://land.dbiz.com$metaImage'; imageUrl = '${ApiConstants.baseUrl}$metaImage';
} else if (metaImage!.startsWith('http')) { } else if (metaImage!.startsWith('http')) {
imageUrl = metaImage!; imageUrl = metaImage!;
} else { } else {
imageUrl = 'https://land.dbiz.com/$metaImage'; imageUrl = '${ApiConstants.baseUrl}/$metaImage';
} }
} else { } else {
imageUrl = 'https://via.placeholder.com/400x300?text=${Uri.encodeComponent(title)}'; imageUrl = 'https://via.placeholder.com/400x300?text=${Uri.encodeComponent(title)}';

View File

@@ -154,33 +154,8 @@ class _NewsDetailPageState extends ConsumerState<NewsDetailPage> {
), ),
), ),
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 // Article Body - Render HTML content
if (article.content != null && article.content!.isNotEmpty) if (article.content != null && article.content!.isNotEmpty)

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:hive_ce/hive.dart'; 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/core/constants/storage_constants.dart';
import 'package:worker/features/products/domain/entities/product.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. /// Maps Frappe Item doctype fields to our ProductModel structure.
/// Frappe fields: /// Frappe fields:
/// - name: Item code (e.g., "GIB20 G02") /// - name: Item code (e.g., "CHG S01P")
/// - item_name: Display name /// - item_name: Display name
/// - description: Product description /// - description: Product description
/// - standard_rate: Price /// - price: Price (from product detail API)
/// - standard_rate: Price (from product list API)
/// - stock_uom: Unit of measurement /// - stock_uom: Unit of measurement
/// - image: Image path /// - thumbnail: Thumbnail image URL
/// - thumbnail: Thumbnail image path /// - image_list: Array of images with image_url
/// - brand: Brand name /// - brand: Brand name
/// - item_group: Category/group /// - item_group_name: Category/group name
/// - custom_link_360: Custom 360 view link /// - custom_link_360: Custom 360 view link
/// - attributes: Array of product attributes (Size, Color, Surface, etc.)
factory ProductModel.fromFrappeJson(Map<String, dynamic> json) { factory ProductModel.fromFrappeJson(Map<String, dynamic> 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 // Handle thumbnail - prepend base URL if needed
String? thumbnailUrl; String? thumbnailUrl;
if (json['thumbnail'] != null && (json['thumbnail'] as String).isNotEmpty) { if (json['thumbnail'] != null && (json['thumbnail'] as String).isNotEmpty) {
final thumbnailPath = json['thumbnail'] as String; final thumbnailPath = json['thumbnail'] as String;
if (thumbnailPath.startsWith('/')) { if (thumbnailPath.startsWith('/')) {
thumbnailUrl = 'https://land.dbiz.com$thumbnailPath'; thumbnailUrl = '${ApiConstants.baseUrl}$thumbnailPath';
} else if (thumbnailPath.startsWith('http')) { } else if (thumbnailPath.startsWith('http')) {
thumbnailUrl = thumbnailPath; thumbnailUrl = thumbnailPath;
} else { } else {
thumbnailUrl = 'https://land.dbiz.com/$thumbnailPath'; thumbnailUrl = '${ApiConstants.baseUrl}/$thumbnailPath';
} }
} }
// Convert single image to list format // Handle image_list array (from product detail API)
final imagesList = imageUrl != null ? [imageUrl] : []; final List<String> imagesList = [];
final Map<String, String> 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<String, dynamic> && 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<String, dynamic> specificationsMap = {};
if (json['attributes'] != null && json['attributes'] is List) {
final attributesData = json['attributes'] as List;
for (final attr in attributesData) {
if (attr is Map<String, dynamic> &&
attr['attribute_name'] != null &&
attr['attribute_value'] != null) {
specificationsMap[attr['attribute_name'] as String] =
attr['attribute_value'] as String;
}
}
}
final now = DateTime.now(); 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( return ProductModel(
productId: json['name'] as String, // Item code productId: json['name'] as String, // Item code
name: json['item_name'] as String? ?? json['name'] as String, name: json['item_name'] as String? ?? json['name'] as String,
description: json['description'] as String?, description: json['description'] as String?,
basePrice: (json['standard_rate'] as num?)?.toDouble() ?? 0.0, basePrice: price,
images: imagesList.isNotEmpty ? jsonEncode(imagesList) : null, images: imagesList.isNotEmpty ? jsonEncode(imagesList) : null,
thumbnail: thumbnailUrl, thumbnail: thumbnailUrl,
imageCaptions: null, // Not provided by API imageCaptions: imageCaptionsMap.isNotEmpty
customLink360: json['custom_link_360'] as String?, ? jsonEncode(imageCaptionsMap)
specifications: json['specifications'] != null
? jsonEncode(json['specifications'])
: null, : 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?, brand: json['brand'] as String?,
unit: json['stock_uom'] as String? ?? '', unit: json['stock_uom'] as String? ?? '',
isActive: (json['disabled'] as int?) == 0, // Frappe uses 'disabled' field isActive: (json['disabled'] as int?) == 0, // Frappe uses 'disabled' field

View File

@@ -11,6 +11,7 @@ import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
import 'package:worker/features/products/presentation/providers/categories_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/providers/products_provider.dart';
import 'package:worker/features/products/presentation/widgets/category_filter_chips.dart'; import 'package:worker/features/products/presentation/widgets/category_filter_chips.dart';
import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart'; import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart';
@@ -31,11 +32,14 @@ class ProductsPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context);
final categoriesAsync = ref.watch(categoriesProvider); final categoriesAsync = ref.watch(categoriesProvider);
final productsAsync = ref.watch(productsProvider); final productsAsync = ref.watch(productsProvider);
final cartItemCount = ref.watch(cartItemCountProvider); final cartItemCount = ref.watch(cartItemCountProvider);
// Preload filter options for better UX when opening filter drawer
ref.watch(productFilterOptionsProvider);
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF4F6F8), // Match HTML background backgroundColor: const Color(0xFFF4F6F8), // Match HTML background
endDrawer: const ProductFilterDrawer(), endDrawer: const ProductFilterDrawer(),

View File

@@ -90,6 +90,9 @@ class FilterOption {
/// 2. Product Brands /// 2. Product Brands
/// 3. Product Attributes /// 3. Product Attributes
/// ///
/// Memory footprint: ~5-15 KB (negligible)
/// Cache strategy: Keep alive for session duration
///
/// Usage: /// Usage:
/// ```dart /// ```dart
/// final filterOptionsAsync = ref.watch(productFilterOptionsProvider); /// final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
@@ -100,7 +103,7 @@ class FilterOption {
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@riverpod @Riverpod(keepAlive: true)
Future<ProductFilterOptions> productFilterOptions(Ref ref) async { Future<ProductFilterOptions> productFilterOptions(Ref ref) async {
try { try {
// Get remote datasource // Get remote datasource

View File

@@ -15,6 +15,9 @@ part of 'product_filter_options_provider.dart';
/// 2. Product Brands /// 2. Product Brands
/// 3. Product Attributes /// 3. Product Attributes
/// ///
/// Memory footprint: ~5-15 KB (negligible)
/// Cache strategy: Keep alive for session duration
///
/// Usage: /// Usage:
/// ```dart /// ```dart
/// final filterOptionsAsync = ref.watch(productFilterOptionsProvider); /// final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
@@ -36,6 +39,9 @@ const productFilterOptionsProvider = ProductFilterOptionsProvider._();
/// 2. Product Brands /// 2. Product Brands
/// 3. Product Attributes /// 3. Product Attributes
/// ///
/// Memory footprint: ~5-15 KB (negligible)
/// Cache strategy: Keep alive for session duration
///
/// Usage: /// Usage:
/// ```dart /// ```dart
/// final filterOptionsAsync = ref.watch(productFilterOptionsProvider); /// final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
@@ -64,6 +70,9 @@ final class ProductFilterOptionsProvider
/// 2. Product Brands /// 2. Product Brands
/// 3. Product Attributes /// 3. Product Attributes
/// ///
/// Memory footprint: ~5-15 KB (negligible)
/// Cache strategy: Keep alive for session duration
///
/// Usage: /// Usage:
/// ```dart /// ```dart
/// final filterOptionsAsync = ref.watch(productFilterOptionsProvider); /// final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
@@ -80,7 +89,7 @@ final class ProductFilterOptionsProvider
argument: null, argument: null,
retry: null, retry: null,
name: r'productFilterOptionsProvider', name: r'productFilterOptionsProvider',
isAutoDispose: true, isAutoDispose: false,
dependencies: null, dependencies: null,
$allTransitiveDependencies: null, $allTransitiveDependencies: null,
); );
@@ -101,4 +110,4 @@ final class ProductFilterOptionsProvider
} }
String _$productFilterOptionsHash() => String _$productFilterOptionsHash() =>
r'394f47113bc2afeea8a0a4548df826900884644b'; r'253586215f05ca2fd1ccae7922b5925150614af0';