update product detail
This commit is contained in:
@@ -81,7 +81,7 @@ class ProductsRemoteDataSource {
|
||||
final productsList = message as List;
|
||||
return productsList
|
||||
.map(
|
||||
(item) => ProductModel.fromFrappeJson(item as Map<String, dynamic>),
|
||||
(item) => ProductModel.fromJson(item as Map<String, dynamic>),
|
||||
)
|
||||
.toList();
|
||||
} on DioException catch (e) {
|
||||
@@ -138,7 +138,7 @@ class ProductsRemoteDataSource {
|
||||
throw Exception('Product not found: $itemCode');
|
||||
}
|
||||
|
||||
return ProductModel.fromFrappeJson(message as Map<String, dynamic>);
|
||||
return ProductModel.fromJson(message as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
throw Exception('Product not found: $itemCode');
|
||||
@@ -206,7 +206,7 @@ class ProductsRemoteDataSource {
|
||||
final productsList = message as List;
|
||||
return productsList
|
||||
.map(
|
||||
(item) => ProductModel.fromFrappeJson(item as Map<String, dynamic>),
|
||||
(item) => ProductModel.fromJson(item as Map<String, dynamic>),
|
||||
)
|
||||
.toList();
|
||||
} on DioException catch (e) {
|
||||
@@ -248,7 +248,7 @@ class ProductsRemoteDataSource {
|
||||
}
|
||||
|
||||
return allProducts
|
||||
.where((product) => product.category == categoryId)
|
||||
.where((product) => product.itemGroupName == categoryId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -495,7 +495,7 @@ class ProductsRemoteDataSource {
|
||||
final productsList = message as List;
|
||||
return productsList
|
||||
.map(
|
||||
(item) => ProductModel.fromFrappeJson(item as Map<String, dynamic>),
|
||||
(item) => ProductModel.fromJson(item as Map<String, dynamic>),
|
||||
)
|
||||
.toList();
|
||||
} on DioException catch (e) {
|
||||
|
||||
@@ -26,7 +26,7 @@ class ProductModel extends HiveObject {
|
||||
this.imageCaptions,
|
||||
this.customLink360,
|
||||
this.specifications,
|
||||
this.category,
|
||||
this.itemGroupName,
|
||||
this.brand,
|
||||
this.unit,
|
||||
this.conversionOfSm,
|
||||
@@ -75,9 +75,9 @@ class ProductModel extends HiveObject {
|
||||
@HiveField(8)
|
||||
final String? specifications;
|
||||
|
||||
/// Product category
|
||||
/// Item group name (from ERPNext)
|
||||
@HiveField(9)
|
||||
final String? category;
|
||||
final String? itemGroupName;
|
||||
|
||||
/// Product brand
|
||||
@HiveField(10)
|
||||
@@ -122,72 +122,24 @@ class ProductModel extends HiveObject {
|
||||
// JSON SERIALIZATION
|
||||
// =========================================================================
|
||||
|
||||
/// Create ProductModel from JSON
|
||||
factory ProductModel.fromJson(Map<String, dynamic> json) {
|
||||
return ProductModel(
|
||||
productId: json['product_id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
basePrice: (json['base_price'] as num).toDouble(),
|
||||
images: json['images'] != null ? jsonEncode(json['images']) : null,
|
||||
thumbnail: json['thumbnail'] as String,
|
||||
imageCaptions: json['image_captions'] != null
|
||||
? jsonEncode(json['image_captions'])
|
||||
: null,
|
||||
customLink360: json['custom_link_360'] as String?,
|
||||
specifications: json['specifications'] != null
|
||||
? jsonEncode(json['specifications'])
|
||||
: null,
|
||||
category: json['category'] as String?,
|
||||
brand: json['brand'] as String?,
|
||||
unit: json['unit'] as String?,
|
||||
conversionOfSm: json['conversion_of_sm'] != null
|
||||
? (json['conversion_of_sm'] as num).toDouble()
|
||||
: null,
|
||||
introAttributes: json['intro_attributes'] != null
|
||||
? jsonEncode(json['intro_attributes'])
|
||||
: null,
|
||||
isActive: json['is_active'] as bool? ?? true,
|
||||
isFeatured: json['is_featured'] as bool? ?? false,
|
||||
erpnextItemCode: json['erpnext_item_code'] as String?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create ProductModel from Frappe ERPNext API JSON
|
||||
/// Create ProductModel from API JSON
|
||||
///
|
||||
/// Maps Frappe Item doctype fields to our ProductModel structure.
|
||||
/// Frappe fields:
|
||||
/// Handles the new Frappe ERPNext API structure with full image URLs.
|
||||
/// API response structure:
|
||||
/// - name: Item code (e.g., "CHG S01P")
|
||||
/// - item_name: Display name
|
||||
/// - description: Product description
|
||||
/// - price: Price (from product detail API)
|
||||
/// - standard_rate: Price (from product list API)
|
||||
/// - stock_uom: Unit of measurement
|
||||
/// - thumbnail: Thumbnail image URL
|
||||
/// - image_list: Array of images with image_url
|
||||
/// - item_group_name: Category
|
||||
/// - brand: Brand name
|
||||
/// - 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<String, dynamic> json) {
|
||||
// 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 = '${ApiConstants.baseUrl}$thumbnailPath';
|
||||
} else if (thumbnailPath.startsWith('http')) {
|
||||
thumbnailUrl = thumbnailPath;
|
||||
} else {
|
||||
thumbnailUrl = '${ApiConstants.baseUrl}/$thumbnailPath';
|
||||
}
|
||||
}
|
||||
/// - thumbnail: Full image URL
|
||||
/// - image_list: Array of {image_name, image_url, description}
|
||||
/// - price: Product price
|
||||
/// - conversion_of_sm: Conversion factor
|
||||
/// - intro_attributes: Array of {code, value}
|
||||
/// - attributes: Array of {attribute, attribute_name, attribute_value}
|
||||
factory ProductModel.fromJson(Map<String, dynamic> json) {
|
||||
final now = DateTime.now();
|
||||
|
||||
// Handle image_list array (from product detail API)
|
||||
// Parse image_list array
|
||||
final List<String> imagesList = [];
|
||||
final Map<String, String> imageCaptionsMap = {};
|
||||
|
||||
@@ -198,26 +150,13 @@ class ProductModel extends HiveObject {
|
||||
final imageUrl = imgData['image_url'] as String;
|
||||
imagesList.add(imageUrl);
|
||||
|
||||
// Store image caption (image_name: A, B, C, etc.)
|
||||
// Store image caption (image_name: Face A, Face B, 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 = {};
|
||||
@@ -233,7 +172,7 @@ class ProductModel extends HiveObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse intro_attributes array for quick reference
|
||||
// Parse intro_attributes array
|
||||
final List<Map<String, String>> introAttributesList = [];
|
||||
if (json['intro_attributes'] != null && json['intro_attributes'] is List) {
|
||||
final introAttrsData = json['intro_attributes'] as List;
|
||||
@@ -249,20 +188,13 @@ class ProductModel extends HiveObject {
|
||||
}
|
||||
}
|
||||
|
||||
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: price,
|
||||
description: json['description'] as String?, // May be null in API
|
||||
basePrice: (json['price'] as num?)?.toDouble() ?? 0.0,
|
||||
images: imagesList.isNotEmpty ? jsonEncode(imagesList) : null,
|
||||
thumbnail: thumbnailUrl ?? '',
|
||||
thumbnail: json['thumbnail'] as String? ?? '',
|
||||
imageCaptions: imageCaptionsMap.isNotEmpty
|
||||
? jsonEncode(imageCaptionsMap)
|
||||
: null,
|
||||
@@ -270,28 +202,24 @@ class ProductModel extends HiveObject {
|
||||
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
|
||||
itemGroupName: json['item_group_name'] as String?,
|
||||
brand: json['brand'] as String?,
|
||||
unit: json['stock_uom'] as String? ?? 'm²',
|
||||
unit: json['currency'] as String?, // Use currency as unit for now
|
||||
conversionOfSm: json['conversion_of_sm'] != null
|
||||
? (json['conversion_of_sm'] as num).toDouble()
|
||||
: null,
|
||||
introAttributes: introAttributesList.isNotEmpty
|
||||
? jsonEncode(introAttributesList)
|
||||
: null,
|
||||
isActive: (json['disabled'] as int?) == 0, // Frappe uses 'disabled' field
|
||||
isFeatured: false, // Not provided by API, default to false
|
||||
erpnextItemCode: json['name'] as String, // Store item code for reference
|
||||
createdAt: json['creation'] != null
|
||||
? DateTime.tryParse(json['creation'] as String) ?? now
|
||||
: now,
|
||||
updatedAt: json['modified'] != null
|
||||
? DateTime.tryParse(json['modified'] as String)
|
||||
: null,
|
||||
isActive: true, // Assume active if returned by API
|
||||
isFeatured: false, // Not provided by API
|
||||
erpnextItemCode: json['item_code'] as String? ?? json['name'] as String,
|
||||
createdAt: now, // Not provided by this API endpoint
|
||||
updatedAt: now,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// Create ProductModel from Wishlist API JSON
|
||||
///
|
||||
/// The wishlist API returns a simplified product structure:
|
||||
@@ -330,7 +258,7 @@ class ProductModel extends HiveObject {
|
||||
imageCaptions: null,
|
||||
customLink360: json['custom_link_360'] as String?,
|
||||
specifications: null,
|
||||
category: json['item_group_name'] as String?,
|
||||
itemGroupName: 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
|
||||
@@ -361,7 +289,7 @@ class ProductModel extends HiveObject {
|
||||
'specifications': specifications != null
|
||||
? jsonDecode(specifications!)
|
||||
: null,
|
||||
'category': category,
|
||||
'item_group_name': itemGroupName,
|
||||
'brand': brand,
|
||||
'unit': unit,
|
||||
'conversion_of_sm': conversionOfSm,
|
||||
@@ -476,7 +404,7 @@ class ProductModel extends HiveObject {
|
||||
String? imageCaptions,
|
||||
String? customLink360,
|
||||
String? specifications,
|
||||
String? category,
|
||||
String? itemGroupName,
|
||||
String? brand,
|
||||
String? unit,
|
||||
double? conversionOfSm,
|
||||
@@ -497,7 +425,7 @@ class ProductModel extends HiveObject {
|
||||
imageCaptions: imageCaptions ?? this.imageCaptions,
|
||||
customLink360: customLink360 ?? this.customLink360,
|
||||
specifications: specifications ?? this.specifications,
|
||||
category: category ?? this.category,
|
||||
itemGroupName: itemGroupName ?? this.itemGroupName,
|
||||
brand: brand ?? this.brand,
|
||||
unit: unit ?? this.unit,
|
||||
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
|
||||
@@ -541,7 +469,7 @@ class ProductModel extends HiveObject {
|
||||
imageCaptions: imageCaptionsMap ?? {},
|
||||
customLink360: customLink360,
|
||||
specifications: specificationsMap ?? {},
|
||||
category: category,
|
||||
itemGroupName: itemGroupName,
|
||||
brand: brand,
|
||||
unit: unit,
|
||||
conversionOfSm: conversionOfSm,
|
||||
|
||||
@@ -26,7 +26,7 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
|
||||
imageCaptions: fields[6] as String?,
|
||||
customLink360: fields[7] as String?,
|
||||
specifications: fields[8] as String?,
|
||||
category: fields[9] as String?,
|
||||
itemGroupName: fields[9] as String?,
|
||||
brand: fields[10] as String?,
|
||||
unit: fields[11] as String?,
|
||||
conversionOfSm: (fields[17] as num?)?.toDouble(),
|
||||
@@ -62,7 +62,7 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
|
||||
..writeByte(8)
|
||||
..write(obj.specifications)
|
||||
..writeByte(9)
|
||||
..write(obj.category)
|
||||
..write(obj.itemGroupName)
|
||||
..writeByte(10)
|
||||
..write(obj.brand)
|
||||
..writeByte(11)
|
||||
|
||||
@@ -20,7 +20,7 @@ class Product {
|
||||
required this.imageCaptions,
|
||||
this.customLink360,
|
||||
required this.specifications,
|
||||
this.category,
|
||||
this.itemGroupName,
|
||||
this.brand,
|
||||
this.unit,
|
||||
this.conversionOfSm,
|
||||
@@ -58,8 +58,8 @@ class Product {
|
||||
/// Product specifications
|
||||
final Map<String, dynamic> specifications;
|
||||
|
||||
/// Category name
|
||||
final String? category;
|
||||
/// Item group name (from ERPNext)
|
||||
final String? itemGroupName;
|
||||
|
||||
/// Brand name
|
||||
final String? brand;
|
||||
@@ -97,8 +97,8 @@ class Product {
|
||||
/// Alias for primaryImage (used by UI widgets)
|
||||
String get imageUrl => primaryImage ?? '';
|
||||
|
||||
/// Category ID (alias for category field)
|
||||
String? get categoryId => category;
|
||||
/// Category ID (alias for itemGroupName field)
|
||||
String? get categoryId => itemGroupName;
|
||||
|
||||
/// Check if product has 360 view
|
||||
bool get has360View => customLink360 != null && customLink360!.isNotEmpty;
|
||||
@@ -152,7 +152,7 @@ class Product {
|
||||
Map<String, String>? imageCaptions,
|
||||
String? customLink360,
|
||||
Map<String, dynamic>? specifications,
|
||||
String? category,
|
||||
String? itemGroupName,
|
||||
String? brand,
|
||||
String? unit,
|
||||
double? conversionOfSm,
|
||||
@@ -173,7 +173,7 @@ class Product {
|
||||
imageCaptions: imageCaptions ?? this.imageCaptions,
|
||||
customLink360: customLink360 ?? this.customLink360,
|
||||
specifications: specifications ?? this.specifications,
|
||||
category: category ?? this.category,
|
||||
itemGroupName: itemGroupName ?? this.itemGroupName,
|
||||
brand: brand ?? this.brand,
|
||||
unit: unit ?? this.unit,
|
||||
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
|
||||
@@ -189,7 +189,7 @@ class Product {
|
||||
@override
|
||||
String toString() {
|
||||
return 'Product(productId: $productId, name: $name, basePrice: $basePrice, '
|
||||
'category: $category, isActive: $isActive, isFeatured: $isFeatured)';
|
||||
'itemGroupName: $itemGroupName, isActive: $isActive, isFeatured: $isFeatured)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -201,7 +201,7 @@ class Product {
|
||||
other.name == name &&
|
||||
other.description == description &&
|
||||
other.basePrice == basePrice &&
|
||||
other.category == category &&
|
||||
other.itemGroupName == itemGroupName &&
|
||||
other.brand == brand &&
|
||||
other.unit == unit &&
|
||||
other.isActive == isActive &&
|
||||
@@ -216,7 +216,7 @@ class Product {
|
||||
name,
|
||||
description,
|
||||
basePrice,
|
||||
category,
|
||||
itemGroupName,
|
||||
brand,
|
||||
unit,
|
||||
isActive,
|
||||
|
||||
@@ -228,13 +228,124 @@ class _SpecificationsTab extends StatelessWidget {
|
||||
};
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(20),
|
||||
margin: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFe0e0e0)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: specs.entries.map((entry) {
|
||||
children: [
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFe0e0e0)),
|
||||
),
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Label
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
color: const Color(0xFFF4F6F8),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Thương hiệu",
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Divider
|
||||
Container(
|
||||
width: 1,
|
||||
color: const Color(0xFFe0e0e0),
|
||||
),
|
||||
|
||||
// Value
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Text(
|
||||
'${product.brand}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey900,
|
||||
height: 1.5,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFe0e0e0)),
|
||||
),
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Label
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
color: const Color(0xFFF4F6F8),
|
||||
child: const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Dòng sản phẩm",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Divider
|
||||
Container(
|
||||
width: 1,
|
||||
color: const Color(0xFFe0e0e0),
|
||||
),
|
||||
|
||||
// Value
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Text(
|
||||
'${product.itemGroupName}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey900,
|
||||
height: 1.5,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
...specs.entries.map((entry) {
|
||||
final isLast = entry == specs.entries.last;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -244,19 +355,25 @@ class _SpecificationsTab extends StatelessWidget {
|
||||
bottom: BorderSide(color: Color(0xFFe0e0e0)),
|
||||
),
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Label
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
color: const Color(0xFFF4F6F8),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
entry.key,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -265,27 +382,30 @@ class _SpecificationsTab extends StatelessWidget {
|
||||
// Divider
|
||||
Container(
|
||||
width: 1,
|
||||
height: 44,
|
||||
color: const Color(0xFFe0e0e0),
|
||||
),
|
||||
|
||||
// Value
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Text(
|
||||
entry.value.toString(),
|
||||
'${entry.value}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey900,
|
||||
height: 1.5,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user