This commit is contained in:
2025-10-28 00:09:46 +07:00
parent 9ebe7c2919
commit de49f564b1
110 changed files with 15392 additions and 3996 deletions

View File

@@ -0,0 +1,62 @@
import '../../../../core/errors/exceptions.dart';
import '../../../../core/network/api_client.dart';
import '../../../../core/network/api_response.dart';
import '../models/product_model.dart';
/// Abstract interface for products remote data source
abstract class ProductsRemoteDataSource {
/// Fetch products from the API
///
/// [warehouseId] - The ID of the warehouse
/// [type] - The operation type ('import' or 'export')
///
/// Returns List<ProductModel>
/// Throws [ServerException] if the API call fails
Future<List<ProductModel>> getProducts(int warehouseId, String type);
}
/// Implementation of ProductsRemoteDataSource using ApiClient
class ProductsRemoteDataSourceImpl implements ProductsRemoteDataSource {
final ApiClient apiClient;
ProductsRemoteDataSourceImpl(this.apiClient);
@override
Future<List<ProductModel>> getProducts(int warehouseId, String type) async {
try {
// Make API call to get all products
final response = await apiClient.get('/portalProduct/getAllProduct');
// Parse the API response using ApiResponse wrapper
final apiResponse = ApiResponse.fromJson(
response.data as Map<String, dynamic>,
(json) => (json as List)
.map((e) => ProductModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
// Check if the API call was successful
if (apiResponse.isSuccess && apiResponse.value != null) {
return apiResponse.value!;
} else {
// Throw exception with error message from API
throw ServerException(
apiResponse.errors.isNotEmpty
? apiResponse.errors.first
: 'Failed to get products',
);
}
} catch (e) {
// Re-throw ServerException as-is
if (e is ServerException) {
rethrow;
}
// Re-throw NetworkException as-is
if (e is NetworkException) {
rethrow;
}
// Wrap other exceptions in ServerException
throw ServerException('Failed to get products: ${e.toString()}');
}
}
}

View File

@@ -0,0 +1,203 @@
import '../../domain/entities/product_entity.dart';
/// Product model - data transfer object
/// Extends ProductEntity and adds serialization capabilities
class ProductModel extends ProductEntity {
const ProductModel({
required super.id,
required super.name,
required super.code,
required super.fullName,
super.description,
super.lotCode,
super.lotNumber,
super.logo,
super.barcode,
required super.quantity,
required super.totalQuantity,
required super.passedQuantity,
super.passedQuantityWeight,
required super.issuedQuantity,
super.issuedQuantityWeight,
required super.piecesInStock,
required super.weightInStock,
required super.weight,
required super.pieces,
required super.conversionRate,
super.percent,
super.price,
required super.isActive,
required super.isConfirm,
super.productStatusId,
required super.productTypeId,
super.orderId,
super.parentId,
super.receiverStageId,
super.order,
super.startDate,
super.endDate,
super.productions,
super.customerProducts,
super.productStages,
super.childrenProducts,
super.productStageWareHouses,
super.productStageDetailWareHouses,
super.productExportExcelSheetDataModels,
super.materialLabels,
super.materials,
super.images,
super.attachmentFiles,
});
/// Create ProductModel from JSON
factory ProductModel.fromJson(Map<String, dynamic> json) {
return ProductModel(
id: json['Id'] ?? 0,
name: json['Name'] ?? '',
code: json['Code'] ?? '',
fullName: json['FullName'] ?? '',
description: json['Description'],
lotCode: json['LotCode'],
lotNumber: json['LotNumber'],
logo: json['Logo'],
barcode: json['Barcode'],
quantity: json['Quantity'] ?? 0,
totalQuantity: json['TotalQuantity'] ?? 0,
passedQuantity: json['PassedQuantity'] ?? 0,
passedQuantityWeight: json['PassedQuantityWeight']?.toDouble(),
issuedQuantity: json['IssuedQuantity'] ?? 0,
issuedQuantityWeight: json['IssuedQuantityWeight']?.toDouble(),
piecesInStock: json['PiecesInStock'] ?? 0,
weightInStock: (json['WeightInStock'] ?? 0).toDouble(),
weight: (json['Weight'] ?? 0).toDouble(),
pieces: json['Pieces'] ?? 0,
conversionRate: (json['ConversionRate'] ?? 0).toDouble(),
percent: json['Percent']?.toDouble(),
price: json['Price']?.toDouble(),
isActive: json['IsActive'] ?? true,
isConfirm: json['IsConfirm'] ?? false,
productStatusId: json['ProductStatusId'],
productTypeId: json['ProductTypeId'] ?? 0,
orderId: json['OrderId'],
parentId: json['ParentId'],
receiverStageId: json['ReceiverStageId'],
order: json['Order'],
startDate: json['StartDate'],
endDate: json['EndDate'],
productions: json['Productions'] ?? [],
customerProducts: json['CustomerProducts'] ?? [],
productStages: json['ProductStages'] ?? [],
childrenProducts: json['ChildrenProducts'],
productStageWareHouses: json['ProductStageWareHouses'],
productStageDetailWareHouses: json['ProductStageDetailWareHouses'],
productExportExcelSheetDataModels:
json['ProductExportExcelSheetDataModels'],
materialLabels: json['MaterialLabels'],
materials: json['Materials'],
images: json['Images'],
attachmentFiles: json['AttachmentFiles'],
);
}
/// Convert ProductModel to JSON
Map<String, dynamic> toJson() {
return {
'Id': id,
'Name': name,
'Code': code,
'FullName': fullName,
'Description': description,
'LotCode': lotCode,
'LotNumber': lotNumber,
'Logo': logo,
'Barcode': barcode,
'Quantity': quantity,
'TotalQuantity': totalQuantity,
'PassedQuantity': passedQuantity,
'PassedQuantityWeight': passedQuantityWeight,
'IssuedQuantity': issuedQuantity,
'IssuedQuantityWeight': issuedQuantityWeight,
'PiecesInStock': piecesInStock,
'WeightInStock': weightInStock,
'Weight': weight,
'Pieces': pieces,
'ConversionRate': conversionRate,
'Percent': percent,
'Price': price,
'IsActive': isActive,
'IsConfirm': isConfirm,
'ProductStatusId': productStatusId,
'ProductTypeId': productTypeId,
'OrderId': orderId,
'ParentId': parentId,
'ReceiverStageId': receiverStageId,
'Order': order,
'StartDate': startDate,
'EndDate': endDate,
'Productions': productions,
'CustomerProducts': customerProducts,
'ProductStages': productStages,
'ChildrenProducts': childrenProducts,
'ProductStageWareHouses': productStageWareHouses,
'ProductStageDetailWareHouses': productStageDetailWareHouses,
'ProductExportExcelSheetDataModels': productExportExcelSheetDataModels,
'MaterialLabels': materialLabels,
'Materials': materials,
'Images': images,
'AttachmentFiles': attachmentFiles,
};
}
/// Convert ProductModel to ProductEntity
ProductEntity toEntity() => this;
/// Create ProductModel from ProductEntity
factory ProductModel.fromEntity(ProductEntity entity) {
return ProductModel(
id: entity.id,
name: entity.name,
code: entity.code,
fullName: entity.fullName,
description: entity.description,
lotCode: entity.lotCode,
lotNumber: entity.lotNumber,
logo: entity.logo,
barcode: entity.barcode,
quantity: entity.quantity,
totalQuantity: entity.totalQuantity,
passedQuantity: entity.passedQuantity,
passedQuantityWeight: entity.passedQuantityWeight,
issuedQuantity: entity.issuedQuantity,
issuedQuantityWeight: entity.issuedQuantityWeight,
piecesInStock: entity.piecesInStock,
weightInStock: entity.weightInStock,
weight: entity.weight,
pieces: entity.pieces,
conversionRate: entity.conversionRate,
percent: entity.percent,
price: entity.price,
isActive: entity.isActive,
isConfirm: entity.isConfirm,
productStatusId: entity.productStatusId,
productTypeId: entity.productTypeId,
orderId: entity.orderId,
parentId: entity.parentId,
receiverStageId: entity.receiverStageId,
order: entity.order,
startDate: entity.startDate,
endDate: entity.endDate,
productions: entity.productions,
customerProducts: entity.customerProducts,
productStages: entity.productStages,
childrenProducts: entity.childrenProducts,
productStageWareHouses: entity.productStageWareHouses,
productStageDetailWareHouses: entity.productStageDetailWareHouses,
productExportExcelSheetDataModels:
entity.productExportExcelSheetDataModels,
materialLabels: entity.materialLabels,
materials: entity.materials,
images: entity.images,
attachmentFiles: entity.attachmentFiles,
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:dartz/dartz.dart';
import '../../../../core/errors/exceptions.dart';
import '../../../../core/errors/failures.dart';
import '../../domain/entities/product_entity.dart';
import '../../domain/repositories/products_repository.dart';
import '../datasources/products_remote_datasource.dart';
/// Implementation of ProductsRepository
/// Handles data operations and error conversion
class ProductsRepositoryImpl implements ProductsRepository {
final ProductsRemoteDataSource remoteDataSource;
ProductsRepositoryImpl(this.remoteDataSource);
@override
Future<Either<Failure, List<ProductEntity>>> getProducts(
int warehouseId,
String type,
) async {
try {
// Fetch products from remote data source
final products = await remoteDataSource.getProducts(warehouseId, type);
// Convert models to entities and return success
return Right(products.map((model) => model.toEntity()).toList());
} on ServerException catch (e) {
// Convert ServerException to ServerFailure
return Left(ServerFailure(e.message));
} on NetworkException catch (e) {
// Convert NetworkException to NetworkFailure
return Left(NetworkFailure(e.message));
} catch (e) {
// Handle any other exceptions
return Left(ServerFailure('Unexpected error: ${e.toString()}'));
}
}
}

View File

@@ -0,0 +1,154 @@
import 'package:equatable/equatable.dart';
/// Product entity - pure domain model
/// Represents a product in the warehouse management system
class ProductEntity extends Equatable {
final int id;
final String name;
final String code;
final String fullName;
final String? description;
final String? lotCode;
final String? lotNumber;
final String? logo;
final String? barcode;
// Quantity fields
final int quantity;
final int totalQuantity;
final int passedQuantity;
final double? passedQuantityWeight;
final int issuedQuantity;
final double? issuedQuantityWeight;
final int piecesInStock;
final double weightInStock;
// Weight and pieces
final double weight;
final int pieces;
final double conversionRate;
final double? percent;
// Price and status
final double? price;
final bool isActive;
final bool isConfirm;
final int? productStatusId;
final int productTypeId;
// Relations
final int? orderId;
final int? parentId;
final int? receiverStageId;
final dynamic order;
// Dates
final String? startDate;
final String? endDate;
// Lists
final List<dynamic> productions;
final List<dynamic> customerProducts;
final List<dynamic> productStages;
final dynamic childrenProducts;
final dynamic productStageWareHouses;
final dynamic productStageDetailWareHouses;
final dynamic productExportExcelSheetDataModels;
final dynamic materialLabels;
final dynamic materials;
final dynamic images;
final dynamic attachmentFiles;
const ProductEntity({
required this.id,
required this.name,
required this.code,
required this.fullName,
this.description,
this.lotCode,
this.lotNumber,
this.logo,
this.barcode,
required this.quantity,
required this.totalQuantity,
required this.passedQuantity,
this.passedQuantityWeight,
required this.issuedQuantity,
this.issuedQuantityWeight,
required this.piecesInStock,
required this.weightInStock,
required this.weight,
required this.pieces,
required this.conversionRate,
this.percent,
this.price,
required this.isActive,
required this.isConfirm,
this.productStatusId,
required this.productTypeId,
this.orderId,
this.parentId,
this.receiverStageId,
this.order,
this.startDate,
this.endDate,
this.productions = const [],
this.customerProducts = const [],
this.productStages = const [],
this.childrenProducts,
this.productStageWareHouses,
this.productStageDetailWareHouses,
this.productExportExcelSheetDataModels,
this.materialLabels,
this.materials,
this.images,
this.attachmentFiles,
});
@override
List<Object?> get props => [
id,
name,
code,
fullName,
description,
lotCode,
lotNumber,
logo,
barcode,
quantity,
totalQuantity,
passedQuantity,
passedQuantityWeight,
issuedQuantity,
issuedQuantityWeight,
piecesInStock,
weightInStock,
weight,
pieces,
conversionRate,
percent,
price,
isActive,
isConfirm,
productStatusId,
productTypeId,
orderId,
parentId,
receiverStageId,
order,
startDate,
endDate,
productions,
customerProducts,
productStages,
childrenProducts,
productStageWareHouses,
productStageDetailWareHouses,
productExportExcelSheetDataModels,
materialLabels,
materials,
images,
attachmentFiles,
];
}

View File

@@ -0,0 +1,18 @@
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/product_entity.dart';
/// Abstract repository interface for products
/// Defines the contract for product data operations
abstract class ProductsRepository {
/// Get products for a specific warehouse and operation type
///
/// [warehouseId] - The ID of the warehouse
/// [type] - The operation type ('import' or 'export')
///
/// Returns Either<Failure, List<ProductEntity>>
Future<Either<Failure, List<ProductEntity>>> getProducts(
int warehouseId,
String type,
);
}

View File

@@ -0,0 +1,25 @@
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/product_entity.dart';
import '../repositories/products_repository.dart';
/// Use case for getting products
/// Encapsulates the business logic for fetching products
class GetProductsUseCase {
final ProductsRepository repository;
GetProductsUseCase(this.repository);
/// Execute the use case
///
/// [warehouseId] - The ID of the warehouse to get products from
/// [type] - The operation type ('import' or 'export')
///
/// Returns Either<Failure, List<ProductEntity>>
Future<Either<Failure, List<ProductEntity>>> call(
int warehouseId,
String type,
) async {
return await repository.getProducts(warehouseId, type);
}
}

View File

@@ -0,0 +1,285 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/di/providers.dart';
import '../widgets/product_list_item.dart';
/// Products list page
/// Displays products for a specific warehouse and operation type
class ProductsPage extends ConsumerStatefulWidget {
final int warehouseId;
final String warehouseName;
final String operationType;
const ProductsPage({
super.key,
required this.warehouseId,
required this.warehouseName,
required this.operationType,
});
@override
ConsumerState<ProductsPage> createState() => _ProductsPageState();
}
class _ProductsPageState extends ConsumerState<ProductsPage> {
@override
void initState() {
super.initState();
// Load products when page is initialized
Future.microtask(() {
ref.read(productsProvider.notifier).loadProducts(
widget.warehouseId,
widget.warehouseName,
widget.operationType,
);
});
}
Future<void> _onRefresh() async {
await ref.read(productsProvider.notifier).refreshProducts();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
// Watch the products state
final productsState = ref.watch(productsProvider);
final products = productsState.products;
final isLoading = productsState.isLoading;
final error = productsState.error;
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Products (${_getOperationTypeDisplay()})',
style: textTheme.titleMedium,
),
Text(
widget.warehouseName,
style: textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _onRefresh,
tooltip: 'Refresh',
),
],
),
body: _buildBody(
isLoading: isLoading,
error: error,
products: products,
theme: theme,
),
);
}
/// Build the body based on the current state
Widget _buildBody({
required bool isLoading,
required String? error,
required List products,
required ThemeData theme,
}) {
return Column(
children: [
// Info header
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withValues(alpha: 0.3),
border: Border(
bottom: BorderSide(
color: theme.colorScheme.outline.withValues(alpha: 0.2),
),
),
),
child: Row(
children: [
Icon(
widget.operationType == 'import'
? Icons.arrow_downward
: Icons.arrow_upward,
color: widget.operationType == 'import'
? Colors.green
: Colors.orange,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_getOperationTypeDisplay(),
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'Warehouse: ${widget.warehouseName}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
),
// Content area
Expanded(
child: _buildContent(
isLoading: isLoading,
error: error,
products: products,
theme: theme,
),
),
],
);
}
/// Build content based on state
Widget _buildContent({
required bool isLoading,
required String? error,
required List products,
required ThemeData theme,
}) {
// Loading state
if (isLoading && products.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading products...'),
],
),
);
}
// Error state
if (error != null && products.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: theme.colorScheme.error,
),
const SizedBox(height: 16),
Text(
'Error',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.error,
),
),
const SizedBox(height: 8),
Text(
error,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: _onRefresh,
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
),
],
),
),
);
}
// Empty state
if (products.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'No Products',
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'No products found for this warehouse and operation type.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: _onRefresh,
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
),
],
),
),
);
}
// Success state - show products list
return RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ProductListItem(
product: product,
onTap: () {
// Handle product tap if needed
// For now, just show a snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Selected: ${product.fullName}'),
duration: const Duration(seconds: 1),
),
);
},
);
},
),
);
}
/// Get display text for operation type
String _getOperationTypeDisplay() {
return widget.operationType == 'import'
? 'Import Products'
: 'Export Products';
}
}

View File

@@ -0,0 +1,108 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/product_entity.dart';
import '../../domain/usecases/get_products_usecase.dart';
/// Products state class
/// Holds the current state of the products feature
class ProductsState {
final List<ProductEntity> products;
final String operationType;
final int? warehouseId;
final String? warehouseName;
final bool isLoading;
final String? error;
const ProductsState({
this.products = const [],
this.operationType = 'import',
this.warehouseId,
this.warehouseName,
this.isLoading = false,
this.error,
});
ProductsState copyWith({
List<ProductEntity>? products,
String? operationType,
int? warehouseId,
String? warehouseName,
bool? isLoading,
String? error,
}) {
return ProductsState(
products: products ?? this.products,
operationType: operationType ?? this.operationType,
warehouseId: warehouseId ?? this.warehouseId,
warehouseName: warehouseName ?? this.warehouseName,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
}
/// Products notifier
/// Manages the products state and business logic
class ProductsNotifier extends StateNotifier<ProductsState> {
final GetProductsUseCase getProductsUseCase;
ProductsNotifier(this.getProductsUseCase) : super(const ProductsState());
/// Load products for a specific warehouse and operation type
///
/// [warehouseId] - The ID of the warehouse
/// [warehouseName] - The name of the warehouse (for display)
/// [type] - The operation type ('import' or 'export')
Future<void> loadProducts(
int warehouseId,
String warehouseName,
String type,
) async {
// Set loading state
state = state.copyWith(
isLoading: true,
error: null,
warehouseId: warehouseId,
warehouseName: warehouseName,
operationType: type,
);
// Call the use case
final result = await getProductsUseCase(warehouseId, type);
// Handle the result
result.fold(
(failure) {
// Handle failure
state = state.copyWith(
isLoading: false,
error: failure.message,
products: [],
);
},
(products) {
// Handle success
state = state.copyWith(
isLoading: false,
error: null,
products: products,
);
},
);
}
/// Clear products list
void clearProducts() {
state = const ProductsState();
}
/// Refresh products
Future<void> refreshProducts() async {
if (state.warehouseId != null) {
await loadProducts(
state.warehouseId!,
state.warehouseName ?? '',
state.operationType,
);
}
}
}

View File

@@ -0,0 +1,241 @@
import 'package:flutter/material.dart';
import '../../domain/entities/product_entity.dart';
/// Reusable product list item widget
/// Displays key product information in a card layout
class ProductListItem extends StatelessWidget {
final ProductEntity product;
final VoidCallback? onTap;
const ProductListItem({
super.key,
required this.product,
this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Product name and code
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.fullName,
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Code: ${product.code}',
style: textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary,
),
),
],
),
),
// Active status indicator
if (product.isActive)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Active',
style: textTheme.labelSmall?.copyWith(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
const Divider(height: 1),
const SizedBox(height: 12),
// Weight and pieces information
Row(
children: [
Expanded(
child: _InfoItem(
label: 'Weight',
value: '${product.weight.toStringAsFixed(2)} kg',
icon: Icons.fitness_center,
),
),
const SizedBox(width: 16),
Expanded(
child: _InfoItem(
label: 'Pieces',
value: product.pieces.toString(),
icon: Icons.inventory_2,
),
),
],
),
const SizedBox(height: 12),
// In stock information
Row(
children: [
Expanded(
child: _InfoItem(
label: 'In Stock (Pieces)',
value: product.piecesInStock.toString(),
icon: Icons.warehouse,
color: product.piecesInStock > 0
? Colors.green
: Colors.orange,
),
),
const SizedBox(width: 16),
Expanded(
child: _InfoItem(
label: 'In Stock (Weight)',
value: '${product.weightInStock.toStringAsFixed(2)} kg',
icon: Icons.scale,
color: product.weightInStock > 0
? Colors.green
: Colors.orange,
),
),
],
),
const SizedBox(height: 12),
// Conversion rate
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Conversion Rate',
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
product.conversionRate.toStringAsFixed(2),
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
],
),
),
// Barcode if available
if (product.barcode != null && product.barcode!.isNotEmpty) ...[
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.qr_code,
size: 16,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
'Barcode: ${product.barcode}',
style: textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
],
],
),
),
),
);
}
}
/// Helper widget for displaying info items
class _InfoItem extends StatelessWidget {
final String label;
final String value;
final IconData icon;
final Color? color;
const _InfoItem({
required this.label,
required this.value,
required this.icon,
this.color,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final effectiveColor = color ?? theme.colorScheme.primary;
return Row(
children: [
Icon(
icon,
size: 20,
color: effectiveColor,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 2),
Text(
value,
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: effectiveColor,
),
),
],
),
),
],
);
}
}