aaa
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import '../../../../core/constants/api_endpoints.dart';
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
import '../../../../core/network/api_response.dart';
|
||||
import '../models/product_detail_request_model.dart';
|
||||
import '../models/product_model.dart';
|
||||
import '../models/product_stage_model.dart';
|
||||
|
||||
/// Abstract interface for products remote data source
|
||||
abstract class ProductsRemoteDataSource {
|
||||
@@ -13,6 +16,14 @@ abstract class ProductsRemoteDataSource {
|
||||
/// Returns List<ProductModel>
|
||||
/// Throws [ServerException] if the API call fails
|
||||
Future<List<ProductModel>> getProducts(int warehouseId, String type);
|
||||
|
||||
/// Get product stages for a product in a warehouse
|
||||
///
|
||||
/// [request] - Request containing warehouseId and productId
|
||||
///
|
||||
/// Returns List<ProductStageModel> with all stages for the product
|
||||
/// Throws [ServerException] if the API call fails
|
||||
Future<List<ProductStageModel>> getProductDetail(ProductDetailRequestModel request);
|
||||
}
|
||||
|
||||
/// Implementation of ProductsRemoteDataSource using ApiClient
|
||||
@@ -59,4 +70,55 @@ class ProductsRemoteDataSourceImpl implements ProductsRemoteDataSource {
|
||||
throw ServerException('Failed to get products: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ProductStageModel>> getProductDetail(
|
||||
ProductDetailRequestModel request) async {
|
||||
try {
|
||||
// Make API call to get product stages
|
||||
final response = await apiClient.post(
|
||||
ApiEndpoints.productDetail,
|
||||
data: request.toJson(),
|
||||
);
|
||||
|
||||
// Parse the API response - the Value field contains a list of stage objects
|
||||
final apiResponse = ApiResponse.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
(json) {
|
||||
// The API returns a list of stages for the product
|
||||
final list = json as List;
|
||||
if (list.isEmpty) {
|
||||
throw const ServerException('Product stages not found');
|
||||
}
|
||||
// Parse all stages from the list
|
||||
return list
|
||||
.map((e) => ProductStageModel.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 product stages',
|
||||
);
|
||||
}
|
||||
} 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 product stages: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/// Request model for getting product details in a warehouse
|
||||
///
|
||||
/// Used to fetch product stage information for a specific warehouse and product
|
||||
class ProductDetailRequestModel {
|
||||
final int warehouseId;
|
||||
final int productId;
|
||||
|
||||
const ProductDetailRequestModel({
|
||||
required this.warehouseId,
|
||||
required this.productId,
|
||||
});
|
||||
|
||||
/// Convert to JSON for API request
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'WareHouseId': warehouseId,
|
||||
'ProductId': productId,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProductDetailRequestModel(warehouseId: $warehouseId, productId: $productId)';
|
||||
}
|
||||
}
|
||||
67
lib/features/products/data/models/product_stage_model.dart
Normal file
67
lib/features/products/data/models/product_stage_model.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import '../../domain/entities/product_stage_entity.dart';
|
||||
|
||||
/// Product stage model for data layer
|
||||
/// Represents a product at a specific production stage
|
||||
class ProductStageModel extends ProductStageEntity {
|
||||
const ProductStageModel({
|
||||
required super.productId,
|
||||
required super.productStageId,
|
||||
required super.actionTypeId,
|
||||
required super.passedQuantity,
|
||||
required super.issuedQuantity,
|
||||
required super.issuedQuantityWeight,
|
||||
required super.passedQuantityWeight,
|
||||
required super.stageName,
|
||||
required super.createdDate,
|
||||
});
|
||||
|
||||
/// Create ProductStageModel from JSON
|
||||
factory ProductStageModel.fromJson(Map<String, dynamic> json) {
|
||||
return ProductStageModel(
|
||||
productId: json['ProductId'] as int,
|
||||
productStageId: json['ProductStageId'] as int?,
|
||||
actionTypeId: json['ActionTypeId'] as int?,
|
||||
passedQuantity: json['PassedQuantity'] as int,
|
||||
issuedQuantity: json['IssuedQuantity'] as int,
|
||||
issuedQuantityWeight: (json['IssuedQuantityWeight'] as num).toDouble(),
|
||||
passedQuantityWeight: (json['PassedQuantityWeight'] as num).toDouble(),
|
||||
stageName: json['StageName'] as String?,
|
||||
createdDate: json['CreatedDate'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'ProductId': productId,
|
||||
'ProductStageId': productStageId,
|
||||
'ActionTypeId': actionTypeId,
|
||||
'PassedQuantity': passedQuantity,
|
||||
'IssuedQuantity': issuedQuantity,
|
||||
'IssuedQuantityWeight': issuedQuantityWeight,
|
||||
'PassedQuantityWeight': passedQuantityWeight,
|
||||
'StageName': stageName,
|
||||
'CreatedDate': createdDate,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to entity
|
||||
ProductStageEntity toEntity() {
|
||||
return ProductStageEntity(
|
||||
productId: productId,
|
||||
productStageId: productStageId,
|
||||
actionTypeId: actionTypeId,
|
||||
passedQuantity: passedQuantity,
|
||||
issuedQuantity: issuedQuantity,
|
||||
issuedQuantityWeight: issuedQuantityWeight,
|
||||
passedQuantityWeight: passedQuantityWeight,
|
||||
stageName: stageName,
|
||||
createdDate: createdDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProductStageModel(productId: $productId, stageName: $stageName, passedQuantity: $passedQuantity)';
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@ import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../domain/entities/product_entity.dart';
|
||||
import '../../domain/entities/product_stage_entity.dart';
|
||||
import '../../domain/repositories/products_repository.dart';
|
||||
import '../datasources/products_remote_datasource.dart';
|
||||
import '../models/product_detail_request_model.dart';
|
||||
|
||||
/// Implementation of ProductsRepository
|
||||
/// Handles data operations and error conversion
|
||||
@@ -34,4 +36,33 @@ class ProductsRepositoryImpl implements ProductsRepository {
|
||||
return Left(ServerFailure('Unexpected error: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<ProductStageEntity>>> getProductDetail(
|
||||
int warehouseId,
|
||||
int productId,
|
||||
) async {
|
||||
try {
|
||||
// Create request model
|
||||
final request = ProductDetailRequestModel(
|
||||
warehouseId: warehouseId,
|
||||
productId: productId,
|
||||
);
|
||||
|
||||
// Fetch product stages from remote data source
|
||||
final stages = await remoteDataSource.getProductDetail(request);
|
||||
|
||||
// Convert models to entities and return success
|
||||
return Right(stages.map((stage) => stage.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()}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Product stage entity - pure domain model
|
||||
/// Represents a product at a specific production stage with quantities
|
||||
class ProductStageEntity extends Equatable {
|
||||
final int productId;
|
||||
final int? productStageId;
|
||||
final int? actionTypeId;
|
||||
final int passedQuantity;
|
||||
final int issuedQuantity;
|
||||
final double issuedQuantityWeight;
|
||||
final double passedQuantityWeight;
|
||||
final String? stageName;
|
||||
final String createdDate;
|
||||
|
||||
const ProductStageEntity({
|
||||
required this.productId,
|
||||
required this.productStageId,
|
||||
required this.actionTypeId,
|
||||
required this.passedQuantity,
|
||||
required this.issuedQuantity,
|
||||
required this.issuedQuantityWeight,
|
||||
required this.passedQuantityWeight,
|
||||
required this.stageName,
|
||||
required this.createdDate,
|
||||
});
|
||||
|
||||
/// Get display name for the stage
|
||||
/// Returns "No Stage" if stageName is null
|
||||
String get displayName => stageName ?? 'No Stage';
|
||||
|
||||
/// Check if this is a valid stage (has a stage name)
|
||||
bool get hasStage => stageName != null && stageName!.isNotEmpty;
|
||||
|
||||
/// Check if this stage has any passed quantity
|
||||
bool get hasPassedQuantity => passedQuantity > 0;
|
||||
|
||||
/// Check if this stage has any issued quantity
|
||||
bool get hasIssuedQuantity => issuedQuantity > 0;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
productId,
|
||||
productStageId,
|
||||
actionTypeId,
|
||||
passedQuantity,
|
||||
issuedQuantity,
|
||||
issuedQuantityWeight,
|
||||
passedQuantityWeight,
|
||||
stageName,
|
||||
createdDate,
|
||||
];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProductStageEntity(productId: $productId, stageName: $stageName, passedQuantity: $passedQuantity)';
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/product_entity.dart';
|
||||
import '../entities/product_stage_entity.dart';
|
||||
|
||||
/// Abstract repository interface for products
|
||||
/// Defines the contract for product data operations
|
||||
@@ -15,4 +16,15 @@ abstract class ProductsRepository {
|
||||
int warehouseId,
|
||||
String type,
|
||||
);
|
||||
|
||||
/// Get product stages for a product in a warehouse
|
||||
///
|
||||
/// [warehouseId] - The ID of the warehouse
|
||||
/// [productId] - The ID of the product
|
||||
///
|
||||
/// Returns Either<Failure, List<ProductStageEntity>>
|
||||
Future<Either<Failure, List<ProductStageEntity>>> getProductDetail(
|
||||
int warehouseId,
|
||||
int productId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/product_stage_entity.dart';
|
||||
import '../repositories/products_repository.dart';
|
||||
|
||||
/// Use case for getting product stages
|
||||
/// Encapsulates the business logic for fetching product stage information
|
||||
class GetProductDetailUseCase {
|
||||
final ProductsRepository repository;
|
||||
|
||||
GetProductDetailUseCase(this.repository);
|
||||
|
||||
/// Execute the use case
|
||||
///
|
||||
/// [warehouseId] - The ID of the warehouse
|
||||
/// [productId] - The ID of the product
|
||||
///
|
||||
/// Returns Either<Failure, List<ProductStageEntity>>
|
||||
Future<Either<Failure, List<ProductStageEntity>>> call(
|
||||
int warehouseId,
|
||||
int productId,
|
||||
) async {
|
||||
return await repository.getProductDetail(warehouseId, productId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/di/providers.dart';
|
||||
import '../../domain/entities/product_stage_entity.dart';
|
||||
|
||||
/// Product detail page
|
||||
/// Displays product stages as chips and shows selected stage information
|
||||
class ProductDetailPage extends ConsumerStatefulWidget {
|
||||
final int warehouseId;
|
||||
final int productId;
|
||||
final String warehouseName;
|
||||
|
||||
const ProductDetailPage({
|
||||
super.key,
|
||||
required this.warehouseId,
|
||||
required this.productId,
|
||||
required this.warehouseName,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ProductDetailPage> createState() => _ProductDetailPageState();
|
||||
}
|
||||
|
||||
class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
late String _providerKey;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_providerKey = '${widget.warehouseId}_${widget.productId}';
|
||||
|
||||
// Load product stages when page is initialized
|
||||
Future.microtask(() {
|
||||
ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail(
|
||||
widget.warehouseId,
|
||||
widget.productId,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onRefresh() async {
|
||||
await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
|
||||
widget.warehouseId,
|
||||
widget.productId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
|
||||
// Watch the product detail state
|
||||
final productDetailState = ref.watch(productDetailProvider(_providerKey));
|
||||
final stages = productDetailState.stages;
|
||||
final selectedStage = productDetailState.selectedStage;
|
||||
final isLoading = productDetailState.isLoading;
|
||||
final error = productDetailState.error;
|
||||
final selectedIndex = productDetailState.selectedStageIndex;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Product Stages',
|
||||
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,
|
||||
stages: stages,
|
||||
selectedStage: selectedStage,
|
||||
selectedIndex: selectedIndex,
|
||||
theme: theme,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody({
|
||||
required bool isLoading,
|
||||
required String? error,
|
||||
required List<ProductStageEntity> stages,
|
||||
required ProductStageEntity? selectedStage,
|
||||
required int selectedIndex,
|
||||
required ThemeData theme,
|
||||
}) {
|
||||
if (isLoading && stages.isEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (error != null && stages.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (stages.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 48,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No stages found',
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Stage chips section
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Production Stages (${stages.length})',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: List.generate(stages.length, (index) {
|
||||
final stage = stages[index];
|
||||
final isSelected = index == selectedIndex;
|
||||
|
||||
return FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(stage.displayName),
|
||||
onSelected: (_) {
|
||||
ref
|
||||
.read(productDetailProvider(_providerKey).notifier)
|
||||
.selectStage(index);
|
||||
},
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
selectedColor: theme.colorScheme.primaryContainer,
|
||||
checkmarkColor: theme.colorScheme.primary,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected
|
||||
? theme.colorScheme.onPrimaryContainer
|
||||
: theme.colorScheme.onSurface,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Stage details section
|
||||
Expanded(
|
||||
child: selectedStage == null
|
||||
? const Center(child: Text('No stage selected'))
|
||||
: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Stage header
|
||||
_buildStageHeader(selectedStage, theme),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quantity information
|
||||
_buildSectionCard(
|
||||
theme: theme,
|
||||
title: 'Quantities',
|
||||
icon: Icons.inventory_outlined,
|
||||
children: [
|
||||
_buildInfoRow('Passed Quantity', '${selectedStage.passedQuantity}'),
|
||||
_buildInfoRow(
|
||||
'Passed Weight',
|
||||
'${selectedStage.passedQuantityWeight.toStringAsFixed(2)} kg',
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildInfoRow('Issued Quantity', '${selectedStage.issuedQuantity}'),
|
||||
_buildInfoRow(
|
||||
'Issued Weight',
|
||||
'${selectedStage.issuedQuantityWeight.toStringAsFixed(2)} kg',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Stage information
|
||||
_buildSectionCard(
|
||||
theme: theme,
|
||||
title: 'Stage Information',
|
||||
icon: Icons.info_outlined,
|
||||
children: [
|
||||
_buildInfoRow('Product ID', '${selectedStage.productId}'),
|
||||
if (selectedStage.productStageId != null)
|
||||
_buildInfoRow('Stage ID', '${selectedStage.productStageId}'),
|
||||
if (selectedStage.actionTypeId != null)
|
||||
_buildInfoRow('Action Type ID', '${selectedStage.actionTypeId}'),
|
||||
_buildInfoRow('Stage Name', selectedStage.displayName),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Status indicators
|
||||
_buildStatusCards(selectedStage, theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStageHeader(ProductStageEntity stage, ThemeData theme) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.build_circle_outlined,
|
||||
size: 32,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
stage.displayName,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Product ID: ${stage.productId}',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionCard({
|
||||
required ThemeData theme,
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required List<Widget> children,
|
||||
}) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusCards(ProductStageEntity stage, ThemeData theme) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
color: stage.hasPassedQuantity
|
||||
? Colors.green.withValues(alpha: 0.1)
|
||||
: Colors.grey.withValues(alpha: 0.1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
stage.hasPassedQuantity ? Icons.check_circle : Icons.cancel,
|
||||
color: stage.hasPassedQuantity ? Colors.green : Colors.grey,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Has Passed',
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Card(
|
||||
color: stage.hasIssuedQuantity
|
||||
? Colors.blue.withValues(alpha: 0.1)
|
||||
: Colors.grey.withValues(alpha: 0.1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
stage.hasIssuedQuantity ? Icons.check_circle : Icons.cancel,
|
||||
color: stage.hasIssuedQuantity ? Colors.blue : Colors.grey,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Has Issued',
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/di/providers.dart';
|
||||
import '../../../../core/router/app_router.dart';
|
||||
import '../widgets/product_list_item.dart';
|
||||
|
||||
/// Products list page
|
||||
@@ -261,13 +262,11 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
|
||||
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),
|
||||
),
|
||||
// Navigate to product detail page
|
||||
context.goToProductDetail(
|
||||
warehouseId: widget.warehouseId,
|
||||
productId: product.id,
|
||||
warehouseName: widget.warehouseName,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../domain/entities/product_stage_entity.dart';
|
||||
import '../../domain/usecases/get_product_detail_usecase.dart';
|
||||
|
||||
/// Product detail state class
|
||||
/// Holds the current state of product stages and selection
|
||||
class ProductDetailState {
|
||||
final List<ProductStageEntity> stages;
|
||||
final int selectedStageIndex;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const ProductDetailState({
|
||||
this.stages = const [],
|
||||
this.selectedStageIndex = 0,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
/// Get the currently selected stage
|
||||
ProductStageEntity? get selectedStage {
|
||||
if (stages.isEmpty || selectedStageIndex >= stages.length) {
|
||||
return null;
|
||||
}
|
||||
return stages[selectedStageIndex];
|
||||
}
|
||||
|
||||
/// Check if there are any stages loaded
|
||||
bool get hasStages => stages.isNotEmpty;
|
||||
|
||||
ProductDetailState copyWith({
|
||||
List<ProductStageEntity>? stages,
|
||||
int? selectedStageIndex,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
}) {
|
||||
return ProductDetailState(
|
||||
stages: stages ?? this.stages,
|
||||
selectedStageIndex: selectedStageIndex ?? this.selectedStageIndex,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Product detail notifier
|
||||
/// Manages the product detail state and business logic
|
||||
class ProductDetailNotifier extends StateNotifier<ProductDetailState> {
|
||||
final GetProductDetailUseCase getProductDetailUseCase;
|
||||
|
||||
ProductDetailNotifier(this.getProductDetailUseCase)
|
||||
: super(const ProductDetailState());
|
||||
|
||||
/// Load product stages for a specific warehouse and product
|
||||
///
|
||||
/// [warehouseId] - The ID of the warehouse
|
||||
/// [productId] - The ID of the product
|
||||
Future<void> loadProductDetail(int warehouseId, int productId) async {
|
||||
// Set loading state
|
||||
state = state.copyWith(
|
||||
isLoading: true,
|
||||
error: null,
|
||||
);
|
||||
|
||||
// Call the use case
|
||||
final result = await getProductDetailUseCase(warehouseId, productId);
|
||||
|
||||
// Handle the result
|
||||
result.fold(
|
||||
(failure) {
|
||||
// Handle failure
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: failure.message,
|
||||
stages: [],
|
||||
);
|
||||
},
|
||||
(stages) {
|
||||
// Handle success - load all stages
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: null,
|
||||
stages: stages,
|
||||
selectedStageIndex: 0, // Select first stage by default
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Select a stage by index
|
||||
void selectStage(int index) {
|
||||
if (index >= 0 && index < state.stages.length) {
|
||||
state = state.copyWith(selectedStageIndex: index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh product stages
|
||||
Future<void> refreshProductDetail(int warehouseId, int productId) async {
|
||||
await loadProductDetail(warehouseId, productId);
|
||||
}
|
||||
|
||||
/// Clear product detail
|
||||
void clearProductDetail() {
|
||||
state = const ProductDetailState();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user