Compare commits
3 Commits
e14ae56c3c
...
4b35d236df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b35d236df | ||
|
|
5cfc56f40d | ||
|
|
0010446298 |
@@ -34,6 +34,13 @@ class ApiEndpoints {
|
|||||||
/// GET: (requires auth token)
|
/// GET: (requires auth token)
|
||||||
static const String profile = '$apiVersion/auth/profile';
|
static const String profile = '$apiVersion/auth/profile';
|
||||||
|
|
||||||
|
// ==================== User Endpoints ====================
|
||||||
|
|
||||||
|
/// Get all users (short info)
|
||||||
|
/// GET: /PortalUser/GetAllMemberUserShortInfo (requires auth token)
|
||||||
|
/// Response: List of users
|
||||||
|
static const String users = '/PortalUser/GetAllMemberUserShortInfo';
|
||||||
|
|
||||||
// ==================== Warehouse Endpoints ====================
|
// ==================== Warehouse Endpoints ====================
|
||||||
|
|
||||||
/// Get all warehouses
|
/// Get all warehouses
|
||||||
@@ -53,17 +60,30 @@ class ApiEndpoints {
|
|||||||
|
|
||||||
// ==================== Product Endpoints ====================
|
// ==================== Product Endpoints ====================
|
||||||
|
|
||||||
/// Get products for a warehouse
|
/// Get products for import (all products)
|
||||||
/// GET: /portalProduct/getAllProduct (requires auth token)
|
/// GET: /portalProduct/getAllProduct (requires auth token)
|
||||||
/// Response: List of products
|
/// Response: List of products
|
||||||
static const String products = '/portalProduct/getAllProduct';
|
static const String products = '/portalProduct/getAllProduct';
|
||||||
|
|
||||||
|
/// Get products for export (products in specific warehouse)
|
||||||
|
/// GET: /portalWareHouse/GetAllProductsInWareHouse?warehouseId={id} (requires auth token)
|
||||||
|
/// Query param: warehouseId (int)
|
||||||
|
/// Response: List of products in warehouse
|
||||||
|
static String productsForExport(int warehouseId) =>
|
||||||
|
'/portalWareHouse/GetAllProductsInWareHouse?warehouseId=$warehouseId';
|
||||||
|
|
||||||
/// Get product stage in warehouse
|
/// Get product stage in warehouse
|
||||||
/// POST: /portalWareHouse/GetProductStageInWareHouse
|
/// POST: /portalWareHouse/GetProductStageInWareHouse
|
||||||
/// Body: { "WareHouseId": int, "ProductId": int }
|
/// Body: { "WareHouseId": int, "ProductId": int }
|
||||||
/// Response: Product details with stage information
|
/// Response: Product details with stage information
|
||||||
static const String productDetail = '/portalWareHouse/GetProductStageInWareHouse';
|
static const String productDetail = '/portalWareHouse/GetProductStageInWareHouse';
|
||||||
|
|
||||||
|
/// Create product warehouse (import/export)
|
||||||
|
/// POST: /portalWareHouse/createProductWareHouse
|
||||||
|
/// Body: Array of product warehouse creation objects
|
||||||
|
/// Response: Created product warehouse record
|
||||||
|
static const String createProductWarehouse = '/portalWareHouse/createProductWareHouse';
|
||||||
|
|
||||||
/// Get product by ID
|
/// Get product by ID
|
||||||
/// GET: (requires auth token)
|
/// GET: (requires auth token)
|
||||||
/// Parameter: productId
|
/// Parameter: productId
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ import '../../features/warehouse/data/repositories/warehouse_repository_impl.dar
|
|||||||
import '../../features/warehouse/domain/repositories/warehouse_repository.dart';
|
import '../../features/warehouse/domain/repositories/warehouse_repository.dart';
|
||||||
import '../../features/warehouse/domain/usecases/get_warehouses_usecase.dart';
|
import '../../features/warehouse/domain/usecases/get_warehouses_usecase.dart';
|
||||||
import '../../features/warehouse/presentation/providers/warehouse_provider.dart';
|
import '../../features/warehouse/presentation/providers/warehouse_provider.dart';
|
||||||
|
import '../../features/users/data/datasources/users_local_datasource.dart';
|
||||||
|
import '../../features/users/data/datasources/users_remote_datasource.dart';
|
||||||
|
import '../../features/users/data/repositories/users_repository_impl.dart';
|
||||||
|
import '../../features/users/domain/repositories/users_repository.dart';
|
||||||
|
import '../../features/users/domain/usecases/get_users_usecase.dart';
|
||||||
|
import '../../features/users/domain/usecases/sync_users_usecase.dart';
|
||||||
|
import '../../features/users/presentation/providers/users_provider.dart';
|
||||||
|
import '../../features/users/presentation/providers/users_state.dart';
|
||||||
import '../network/api_client.dart';
|
import '../network/api_client.dart';
|
||||||
import '../storage/secure_storage.dart';
|
import '../storage/secure_storage.dart';
|
||||||
|
|
||||||
@@ -392,6 +400,105 @@ final productDetailErrorProvider = Provider.family<String?, String>((ref, key) {
|
|||||||
return state.error;
|
return state.error;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// ========================================================================
|
||||||
|
/// USERS FEATURE PROVIDERS
|
||||||
|
/// ========================================================================
|
||||||
|
/// Providers for users feature following clean architecture
|
||||||
|
|
||||||
|
// Data Layer
|
||||||
|
|
||||||
|
/// Users local data source provider
|
||||||
|
/// Handles local storage operations for users using Hive
|
||||||
|
final usersLocalDataSourceProvider = Provider<UsersLocalDataSource>((ref) {
|
||||||
|
return UsersLocalDataSourceImpl();
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Users remote data source provider
|
||||||
|
/// Handles API calls for users
|
||||||
|
final usersRemoteDataSourceProvider = Provider<UsersRemoteDataSource>((ref) {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
return UsersRemoteDataSourceImpl(apiClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Users repository provider
|
||||||
|
/// Implements domain repository interface
|
||||||
|
/// Coordinates between local and remote data sources
|
||||||
|
final usersRepositoryProvider = Provider<UsersRepository>((ref) {
|
||||||
|
final remoteDataSource = ref.watch(usersRemoteDataSourceProvider);
|
||||||
|
final localDataSource = ref.watch(usersLocalDataSourceProvider);
|
||||||
|
return UsersRepositoryImpl(
|
||||||
|
remoteDataSource: remoteDataSource,
|
||||||
|
localDataSource: localDataSource,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Domain Layer
|
||||||
|
|
||||||
|
/// Get users use case provider
|
||||||
|
/// Encapsulates user fetching business logic (local-first strategy)
|
||||||
|
final getUsersUseCaseProvider = Provider<GetUsersUseCase>((ref) {
|
||||||
|
final repository = ref.watch(usersRepositoryProvider);
|
||||||
|
return GetUsersUseCase(repository);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Sync users use case provider
|
||||||
|
/// Encapsulates user syncing business logic (force refresh from API)
|
||||||
|
final syncUsersUseCaseProvider = Provider<SyncUsersUseCase>((ref) {
|
||||||
|
final repository = ref.watch(usersRepositoryProvider);
|
||||||
|
return SyncUsersUseCase(repository);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Presentation Layer
|
||||||
|
|
||||||
|
/// Users state notifier provider
|
||||||
|
/// Manages users state including list, loading, and errors
|
||||||
|
final usersProvider = StateNotifierProvider<UsersNotifier, UsersState>((ref) {
|
||||||
|
final getUsersUseCase = ref.watch(getUsersUseCaseProvider);
|
||||||
|
final syncUsersUseCase = ref.watch(syncUsersUseCaseProvider);
|
||||||
|
return UsersNotifier(
|
||||||
|
getUsersUseCase: getUsersUseCase,
|
||||||
|
syncUsersUseCase: syncUsersUseCase,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Convenient providers for users state
|
||||||
|
|
||||||
|
/// Provider to get list of users
|
||||||
|
/// Usage: ref.watch(usersListProvider)
|
||||||
|
final usersListProvider = Provider((ref) {
|
||||||
|
final usersState = ref.watch(usersProvider);
|
||||||
|
return usersState.users;
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Provider to check if users are loading
|
||||||
|
/// Usage: ref.watch(isUsersLoadingProvider)
|
||||||
|
final isUsersLoadingProvider = Provider<bool>((ref) {
|
||||||
|
final usersState = ref.watch(usersProvider);
|
||||||
|
return usersState.isLoading;
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Provider to check if users list has items
|
||||||
|
/// Usage: ref.watch(hasUsersProvider)
|
||||||
|
final hasUsersProvider = Provider<bool>((ref) {
|
||||||
|
final usersState = ref.watch(usersProvider);
|
||||||
|
return usersState.users.isNotEmpty;
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Provider to get users count
|
||||||
|
/// Usage: ref.watch(usersCountProvider)
|
||||||
|
final usersCountProvider = Provider<int>((ref) {
|
||||||
|
final usersState = ref.watch(usersProvider);
|
||||||
|
return usersState.users.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Provider to get users error
|
||||||
|
/// Returns null if no error
|
||||||
|
/// Usage: ref.watch(usersErrorProvider)
|
||||||
|
final usersErrorProvider = Provider<String?>((ref) {
|
||||||
|
final usersState = ref.watch(usersProvider);
|
||||||
|
return usersState.error;
|
||||||
|
});
|
||||||
|
|
||||||
/// ========================================================================
|
/// ========================================================================
|
||||||
/// USAGE EXAMPLES
|
/// USAGE EXAMPLES
|
||||||
/// ========================================================================
|
/// ========================================================================
|
||||||
|
|||||||
@@ -81,32 +81,24 @@ class AppRouter {
|
|||||||
),
|
),
|
||||||
|
|
||||||
/// Products List Route
|
/// Products List Route
|
||||||
/// Path: /products
|
/// Path: /products/:warehouseId/:operationType
|
||||||
/// Takes warehouse, warehouseName, and operationType as extra parameter
|
/// Query params: name (warehouse name)
|
||||||
/// Shows products for selected warehouse and operation
|
/// Shows products for selected warehouse and operation
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/products',
|
path: '/products/:warehouseId/:operationType',
|
||||||
name: 'products',
|
name: 'products',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final params = state.extra as Map<String, dynamic>?;
|
// Extract path parameters
|
||||||
|
final warehouseIdStr = state.pathParameters['warehouseId'];
|
||||||
|
final operationType = state.pathParameters['operationType'];
|
||||||
|
|
||||||
if (params == null) {
|
// Extract query parameter
|
||||||
// If no params, redirect to warehouses
|
final warehouseName = state.uri.queryParameters['name'];
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
context.go('/warehouses');
|
|
||||||
});
|
|
||||||
return const _ErrorScreen(
|
|
||||||
message: 'Product parameters are required',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract required parameters
|
// Parse and validate parameters
|
||||||
final warehouse = params['warehouse'] as WarehouseEntity?;
|
final warehouseId = int.tryParse(warehouseIdStr ?? '');
|
||||||
final warehouseName = params['warehouseName'] as String?;
|
|
||||||
final operationType = params['operationType'] as String?;
|
|
||||||
|
|
||||||
// Validate parameters
|
if (warehouseId == null || warehouseName == null || operationType == null) {
|
||||||
if (warehouse == null || warehouseName == null || operationType == null) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.go('/warehouses');
|
context.go('/warehouses');
|
||||||
});
|
});
|
||||||
@@ -116,7 +108,7 @@ class AppRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ProductsPage(
|
return ProductsPage(
|
||||||
warehouseId: warehouse.id,
|
warehouseId: warehouseId,
|
||||||
warehouseName: warehouseName,
|
warehouseName: warehouseName,
|
||||||
operationType: operationType,
|
operationType: operationType,
|
||||||
);
|
);
|
||||||
@@ -124,34 +116,28 @@ class AppRouter {
|
|||||||
),
|
),
|
||||||
|
|
||||||
/// Product Detail Route
|
/// Product Detail Route
|
||||||
/// Path: /product-detail
|
/// Path: /product-detail/:warehouseId/:productId/:operationType
|
||||||
/// Takes warehouseId, productId, warehouseName, and optional stageId as extra parameter
|
/// Query params: name (warehouse name), stageId (optional)
|
||||||
/// Shows detailed information for a specific product
|
/// Shows detailed information for a specific product
|
||||||
/// If stageId is provided, only that stage is shown, otherwise all stages are shown
|
/// If stageId is provided, only that stage is shown, otherwise all stages are shown
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/product-detail',
|
path: '/product-detail/:warehouseId/:productId/:operationType',
|
||||||
name: 'product-detail',
|
name: 'product-detail',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final params = state.extra as Map<String, dynamic>?;
|
// Extract path parameters
|
||||||
|
final warehouseIdStr = state.pathParameters['warehouseId'];
|
||||||
|
final productIdStr = state.pathParameters['productId'];
|
||||||
|
final operationType = state.pathParameters['operationType'] ?? 'import';
|
||||||
|
|
||||||
if (params == null) {
|
// Extract query parameters
|
||||||
// If no params, redirect to warehouses
|
final warehouseName = state.uri.queryParameters['name'];
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
final stageIdStr = state.uri.queryParameters['stageId'];
|
||||||
context.go('/warehouses');
|
|
||||||
});
|
|
||||||
return const _ErrorScreen(
|
|
||||||
message: 'Product detail parameters are required',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract required parameters
|
// Parse and validate parameters
|
||||||
final warehouseId = params['warehouseId'] as int?;
|
final warehouseId = int.tryParse(warehouseIdStr ?? '');
|
||||||
final productId = params['productId'] as int?;
|
final productId = int.tryParse(productIdStr ?? '');
|
||||||
final warehouseName = params['warehouseName'] as String?;
|
final stageId = stageIdStr != null ? int.tryParse(stageIdStr) : null;
|
||||||
// Extract optional stageId
|
|
||||||
final stageId = params['stageId'] as int?;
|
|
||||||
|
|
||||||
// Validate parameters
|
|
||||||
if (warehouseId == null || productId == null || warehouseName == null) {
|
if (warehouseId == null || productId == null || warehouseName == null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.go('/warehouses');
|
context.go('/warehouses');
|
||||||
@@ -166,6 +152,7 @@ class AppRouter {
|
|||||||
productId: productId,
|
productId: productId,
|
||||||
warehouseName: warehouseName,
|
warehouseName: warehouseName,
|
||||||
stageId: stageId,
|
stageId: stageId,
|
||||||
|
operationType: operationType,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -360,18 +347,11 @@ extension AppRouterExtension on BuildContext {
|
|||||||
///
|
///
|
||||||
/// [warehouse] - Selected warehouse entity
|
/// [warehouse] - Selected warehouse entity
|
||||||
/// [operationType] - Either 'import' or 'export'
|
/// [operationType] - Either 'import' or 'export'
|
||||||
void goToProducts({
|
void pushToProducts({
|
||||||
required WarehouseEntity warehouse,
|
required WarehouseEntity warehouse,
|
||||||
required String operationType,
|
required String operationType,
|
||||||
}) {
|
}) {
|
||||||
go(
|
push('/products/${warehouse.id}/$operationType?name=${Uri.encodeQueryComponent(warehouse.name)}');
|
||||||
'/products',
|
|
||||||
extra: {
|
|
||||||
'warehouse': warehouse,
|
|
||||||
'warehouseName': warehouse.name,
|
|
||||||
'operationType': operationType,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigate to product detail page
|
/// Navigate to product detail page
|
||||||
@@ -379,22 +359,23 @@ extension AppRouterExtension on BuildContext {
|
|||||||
/// [warehouseId] - ID of the warehouse
|
/// [warehouseId] - ID of the warehouse
|
||||||
/// [productId] - ID of the product to view
|
/// [productId] - ID of the product to view
|
||||||
/// [warehouseName] - Name of the warehouse (for display)
|
/// [warehouseName] - Name of the warehouse (for display)
|
||||||
|
/// [operationType] - Either 'import' or 'export'
|
||||||
/// [stageId] - Optional ID of specific stage to show (if null, show all stages)
|
/// [stageId] - Optional ID of specific stage to show (if null, show all stages)
|
||||||
void goToProductDetail({
|
void goToProductDetail({
|
||||||
required int warehouseId,
|
required int warehouseId,
|
||||||
required int productId,
|
required int productId,
|
||||||
required String warehouseName,
|
required String warehouseName,
|
||||||
|
required String operationType,
|
||||||
int? stageId,
|
int? stageId,
|
||||||
}) {
|
}) {
|
||||||
push(
|
final queryParams = <String, String>{
|
||||||
'/product-detail',
|
'name': warehouseName,
|
||||||
extra: {
|
if (stageId != null) 'stageId': stageId.toString(),
|
||||||
'warehouseId': warehouseId,
|
};
|
||||||
'productId': productId,
|
final queryString = queryParams.entries
|
||||||
'warehouseName': warehouseName,
|
.map((e) => '${e.key}=${Uri.encodeQueryComponent(e.value)}')
|
||||||
if (stageId != null) 'stageId': stageId,
|
.join('&');
|
||||||
},
|
push('/product-detail/$warehouseId/$productId/$operationType?$queryString');
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pop current route
|
/// Pop current route
|
||||||
@@ -421,11 +402,13 @@ extension AppRouterNamedExtension on BuildContext {
|
|||||||
}) {
|
}) {
|
||||||
goNamed(
|
goNamed(
|
||||||
'products',
|
'products',
|
||||||
extra: {
|
pathParameters: {
|
||||||
'warehouse': warehouse,
|
'warehouseId': warehouse.id.toString(),
|
||||||
'warehouseName': warehouse.name,
|
|
||||||
'operationType': operationType,
|
'operationType': operationType,
|
||||||
},
|
},
|
||||||
|
queryParameters: {
|
||||||
|
'name': warehouse.name,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ curl --request POST \
|
|||||||
}'
|
}'
|
||||||
|
|
||||||
|
|
||||||
#Get products
|
#Get products for import
|
||||||
curl --request GET \
|
curl --request GET \
|
||||||
--url https://dotnet.elidev.info:8157/ws/portalProduct/getAllProduct \
|
--url https://dotnet.elidev.info:8157/ws/portalProduct/getAllProduct \
|
||||||
--compressed \
|
--compressed \
|
||||||
@@ -54,6 +54,22 @@ curl --request GET \
|
|||||||
--header 'Sec-Fetch-Site: same-site' \
|
--header 'Sec-Fetch-Site: same-site' \
|
||||||
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0'
|
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0'
|
||||||
|
|
||||||
|
|
||||||
|
#Get product for export
|
||||||
|
curl 'https://dotnet.elidev.info:8157/ws/portalWareHouse/GetAllProductsInWareHouse?warehouseId=1' \
|
||||||
|
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0' \
|
||||||
|
-H 'Accept: application/json, text/plain, */*' \
|
||||||
|
-H 'Accept-Language: en-US,en;q=0.5' \
|
||||||
|
-H 'Accept-Encoding: gzip, deflate, br, zstd' \
|
||||||
|
-H 'AccessToken: 1k5fXyQVXtGkfjwS4g2USldGyLSA7Zwa2jdj5tLe+3YfSDlk02aYgqsFh5xArkdL4N529x7IYJOGrLJJgLBPNVgD51zFfEYBzfJmMH2RUm7iegvDJaMCLISySw0zd6kcsqeJi7vtuybgY2NDPxDgiSOj4wX417PzB8AVg5bl1ZAAJ3LcVAqqtA1PDTU5ZU1QQYapNeBNxAHjnd2ojTZK1GJBIyY5Gd8P9gB880ppAKq8manNMZYsa4d8tkYf0SJUul2aqLIWJAwDGORpPmfjqkN4hMh85xAfPTZi6m4DdI0u2rHDMLaZ8eIsV16qA8wimSDnWi0VeG0SZ4ugbCdJAi3t1/uICTftiy06PJEkulBLV+h2xS/7SlmEY2xoN5ISi++3FNqsFPGa9QH6akGu2C7IXEUBCg3iGJx0uL+vULmVqk5OJIXdqiKVQ366hvhPlK2AM1zbh49x/ngibe08483WTL5uAY/fsKuBxQCpTc2368Gqhpd7QRtZFKpzikhyTWsR3nQIi6ExSstCeFbe8ehgo0PuTPZNHH5IHTc49snH6IZrSbR+F62Wu/D+4DlvMTK/ktG6LVQ3r3jSJC5MAQDV5Q9WK3RvsWMPvZrsaVW/Exz0GBgWP4W0adADg7MFSlnGDOJm6I4fCLHZIJCUww50L6iNmzvrdibrQT5jKACVgNquMZCfeZlf3m2BwUx9T6J45lAePpJ+QaMh+2voFqRiOLi98MLqOG6TW7z96sadzFVR9YU1xwM51jQDjnUlrXt0+msq29Jqt8LoCyQsG4r3RgS/tUJhximq11MDXsSXanpYM7jesjr8mAG4qjYN6z6c1Gl5N0dhcDF4HeEaIlNIgZ75FqtXZnLqvhHPyk6L2iR2ZT15nobZxLzOUad4a0OymUDUv7xuEBdEk5kmzZLDpbOxrKiyMpGSlbBhEoBMoA0u6ZKtBGQfCJ02s6Ri0WhLLM4XJCjGrpoEkTUuZ7YG39Zva19HGV0kkxeFYkG0lnZBO6jCggem5f+S2NQvXP/kUrWX1GeQFCq5PScvwJexLsbh0LKC2MGovkecoBKtNIK21V6ztvWL8lThJAl9' \
|
||||||
|
-H 'AppID: Minhthu2016' \
|
||||||
|
-H 'Origin: https://dotnet.elidev.info:8158' \
|
||||||
|
-H 'Connection: keep-alive' \
|
||||||
|
-H 'Referer: https://dotnet.elidev.info:8158/' \
|
||||||
|
-H 'Sec-Fetch-Dest: empty' \
|
||||||
|
-H 'Sec-Fetch-Mode: cors' \
|
||||||
|
-H 'Sec-Fetch-Site: same-site'
|
||||||
|
|
||||||
#Get product by id
|
#Get product by id
|
||||||
curl --request POST \
|
curl --request POST \
|
||||||
--url https://dotnet.elidev.info:8157/ws/portalWareHouse/GetProductStageInWareHouse \
|
--url https://dotnet.elidev.info:8157/ws/portalWareHouse/GetProductStageInWareHouse \
|
||||||
@@ -76,3 +92,39 @@ curl --request POST \
|
|||||||
"WareHouseId": 7,
|
"WareHouseId": 7,
|
||||||
"ProductId": 11
|
"ProductId": 11
|
||||||
}'
|
}'
|
||||||
|
|
||||||
|
#Create import product
|
||||||
|
curl 'https://dotnet.elidev.info:8157/ws/portalWareHouse/createProductWareHouse' \
|
||||||
|
-X POST \
|
||||||
|
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0' \
|
||||||
|
-H 'Accept: application/json, text/plain, */*' \
|
||||||
|
-H 'Accept-Language: en-US,en;q=0.5' \
|
||||||
|
-H 'Accept-Encoding: gzip, deflate, br, zstd' \
|
||||||
|
-H 'AccessToken: 1k5fXyQVXtGkfjwS4g2USldGyLSA7Zwa2jdj5tLe+3YfSDlk02aYgqsFh5xArkdL4N529x7IYJOGrLJJgLBPNVgD51zFfEYBzfJmMH2RUm7iegvDJaMCLISySw0zd6kcsqeJi7vtuybgY2NDPxDgiSOj4wX417PzB8AVg5bl1ZAAJ3LcVAqqtA1PDTU5ZU1QQYapNeBNxAHjnd2ojTZK1GJBIyY5Gd8P9gB880ppAKq8manNMZYsa4d8tkYf0SJUul2aqLIWJAwDGORpPmfjqkN4hMh85xAfPTZi6m4DdI0u2rHDMLaZ8eIsV16qA8wimSDnWi0VeG0SZ4ugbCdJAi3t1/uICTftiy06PJEkulBLV+h2xS/7SlmEY2xoN5ISi++3FNqsFPGa9QH6akGu2C7IXEUBCg3iGJx0uL+vULmVqk5OJIXdqiKVQ366hvhPlK2AM1zbh49x/ngibe08483WTL5uAY/fsKuBxQCpTc2368Gqhpd7QRtZFKpzikhyTWsR3nQIi6ExSstCeFbe8ehgo0PuTPZNHH5IHTc49snH6IZrSbR+F62Wu/D+4DlvMTK/ktG6LVQ3r3jSJC5MAQDV5Q9WK3RvsWMPvZrsaVW/Exz0GBgWP4W0adADg7MFSlnGDOJm6I4fCLHZIJCUww50L6iNmzvrdibrQT5jKACVgNquMZCfeZlf3m2BwUx9T6J45lAePpJ+QaMh+2voFqRiOLi98MLqOG6TW7z96sadzFVR9YU1xwM51jQDjnUlrXt0+msq29Jqt8LoCyQsG4r3RgS/tUJhximq11MDXsSXanpYM7jesjr8mAG4qjYN6z6c1Gl5N0dhcDF4HeEaIlNIgZ75FqtXZnLqvhHPyk6L2iR2ZT15nobZxLzOUad4a0OymUDUv7xuEBdEk5kmzZLDpbOxrKiyMpGSlbBhEoBMoA0u6ZKtBGQfCJ02s6Ri0WhLLM4XJCjGrpoEkTUuZ7YG39Zva19HGV0kkxeFYkG0lnZBO6jCggem5f+S2NQvXP/kUrWX1GeQFCq5PScvwJexLsbh0LKC2MGovkecoBKtNIK21V6ztvWL8lThJAl9' \
|
||||||
|
-H 'AppID: Minhthu2016' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'Origin: https://dotnet.elidev.info:8158' \
|
||||||
|
-H 'Connection: keep-alive' \
|
||||||
|
-H 'Referer: https://dotnet.elidev.info:8158/' \
|
||||||
|
-H 'Sec-Fetch-Dest: empty' \
|
||||||
|
-H 'Sec-Fetch-Mode: cors' \
|
||||||
|
-H 'Sec-Fetch-Site: same-site' \
|
||||||
|
-H 'Priority: u=0' \
|
||||||
|
--data-raw $'[{"TypeId":4,"ProductId":11,"StageId":3,"OrderId":null,"RecordDate":"2025-10-28T08:19:20.418Z","PassedQuantityWeight":0.5,"PassedQuantity":5,"IssuedQuantityWeight":0.1,"IssuedQuantity":1,"ResponsibleUserId":12043,"Description":"","ProductName":"Th\xe9p 435","ProductCode":"SCM435","StockPassedQuantityWeight":0,"StockPassedQuantity":0,"StockIssuedQuantity":0,"StockIssuedQuantityWeight":0,"ReceiverUserId":12120,"ActionTypeId":1,"WareHouseId":1,"ProductStageId":3,"IsConfirm":true}]'
|
||||||
|
|
||||||
|
#Get users
|
||||||
|
curl --request GET \
|
||||||
|
--url https://dotnet.elidev.info:8157/ws/PortalUser/GetAllMemberUserShortInfo \
|
||||||
|
--compressed \
|
||||||
|
--header 'Accept: application/json, text/plain, */*' \
|
||||||
|
--header 'Accept-Encoding: gzip, deflate, br, zstd' \
|
||||||
|
--header 'Accept-Language: en-US,en;q=0.5' \
|
||||||
|
--header 'AccessToken: 1k5fXyQVXtGkfjwS4g2USldGyLSA7Zwa2jdj5tLe+3YfSDlk02aYgqsFh5xArkdL4N529x7IYJOGrLJJgLBPNVgD51zFfEYBzfJmMH2RUm7iegvDJaMCLISySw0zd6kcsqeJi7vtuybgY2NDPxDgiSOj4wX417PzB8AVg5bl1ZAAJ3LcVAqqtA1PDTU5ZU1QQYapNeBNxAHjnd2ojTZK1GJBIyY5Gd8P9gB880ppAKq8manNMZYsa4d8tkYf0SJUul2aqLIWJAwDGORpPmfjqkN4hMh85xAfPTZi6m4DdI0u2rHDMLaZ8eIsV16qA8wimSDnWi0VeG0SZ4ugbCdJAi3t1/uICTftiy06PJEkulBLV+h2xS/7SlmEY2xoN5ISi++3FNqsFPGa9QH6akGu2C7IXEUBCg3iGJx0uL+vULmVqk5OJIXdqiKVQ366hvhPlK2AM1zbh49x/ngibe08483WTL5uAY/fsKuBxQCpTc2368Gqhpd7QRtZFKpzikhyTWsR3nQIi6ExSstCeFbe8ehgo0PuTPZNHH5IHTc49snH6IZrSbR+F62Wu/D+4DlvMTK/ktG6LVQ3r3jSJC5MAQDV5Q9WK3RvsWMPvZrsaVW/Exz0GBgWP4W0adADg7MFSlnGDOJm6I4fCLHZIJCUww50L6iNmzvrdibrQT5jKACVgNquMZCfeZlf3m2BwUx9T6J45lAePpJ+QaMh+2voFqRiOLi98MLqOG6TW7z96sadzFVR9YU1xwM51jQDjnUlrXt0+msq29Jqt8LoCyQsG4r3RgS/tUJhximq11MDXsSrJm6RubpKWl/MnF3QxcLTSwFE/SrGoGhnJnH5ILxZdyMN4PJjELeq3g8V5nEEm6lE/WNyvRMskel+Ods3XQIvE6o8KblUmFeM1rOIBkJbUVX7Ghaj0RNpvar86fF85BEozLBcED2XGkDANhGivuhqyrDpEOYjCwuC0eOjdj92fxlTTyo33ioR3xKcYFVlgMTlRX26sQDayf8hsPIBoDQtMHGKFfC2BJx4ujKTxtead8uz7c+CrlJuTbUkk+bp+wEUvKW5TvlX8HXKJWVLm05qZ7KmSLpsp35Iih2tZbBU+g==' \
|
||||||
|
--header 'AppID: Minhthu2016' \
|
||||||
|
--header 'Connection: keep-alive' \
|
||||||
|
--header 'Origin: https://dotnet.elidev.info:8158' \
|
||||||
|
--header 'Referer: https://dotnet.elidev.info:8158/' \
|
||||||
|
--header 'Sec-Fetch-Dest: empty' \
|
||||||
|
--header 'Sec-Fetch-Mode: cors' \
|
||||||
|
--header 'Sec-Fetch-Site: same-site' \
|
||||||
|
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0'
|
||||||
@@ -2,6 +2,7 @@ import '../../../../core/constants/api_endpoints.dart';
|
|||||||
import '../../../../core/errors/exceptions.dart';
|
import '../../../../core/errors/exceptions.dart';
|
||||||
import '../../../../core/network/api_client.dart';
|
import '../../../../core/network/api_client.dart';
|
||||||
import '../../../../core/network/api_response.dart';
|
import '../../../../core/network/api_response.dart';
|
||||||
|
import '../models/create_product_warehouse_request.dart';
|
||||||
import '../models/product_detail_request_model.dart';
|
import '../models/product_detail_request_model.dart';
|
||||||
import '../models/product_model.dart';
|
import '../models/product_model.dart';
|
||||||
import '../models/product_stage_model.dart';
|
import '../models/product_stage_model.dart';
|
||||||
@@ -24,6 +25,14 @@ abstract class ProductsRemoteDataSource {
|
|||||||
/// Returns List<ProductStageModel> with all stages for the product
|
/// Returns List<ProductStageModel> with all stages for the product
|
||||||
/// Throws [ServerException] if the API call fails
|
/// Throws [ServerException] if the API call fails
|
||||||
Future<List<ProductStageModel>> getProductDetail(ProductDetailRequestModel request);
|
Future<List<ProductStageModel>> getProductDetail(ProductDetailRequestModel request);
|
||||||
|
|
||||||
|
/// Create product warehouse entry (import/export operation)
|
||||||
|
///
|
||||||
|
/// [request] - Request containing all product warehouse details
|
||||||
|
///
|
||||||
|
/// Returns void on success
|
||||||
|
/// Throws [ServerException] if the API call fails
|
||||||
|
Future<void> createProductWarehouse(CreateProductWarehouseRequest request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of ProductsRemoteDataSource using ApiClient
|
/// Implementation of ProductsRemoteDataSource using ApiClient
|
||||||
@@ -35,8 +44,13 @@ class ProductsRemoteDataSourceImpl implements ProductsRemoteDataSource {
|
|||||||
@override
|
@override
|
||||||
Future<List<ProductModel>> getProducts(int warehouseId, String type) async {
|
Future<List<ProductModel>> getProducts(int warehouseId, String type) async {
|
||||||
try {
|
try {
|
||||||
// Make API call to get all products
|
// Choose endpoint based on operation type
|
||||||
final response = await apiClient.get('/portalProduct/getAllProduct');
|
final endpoint = type == 'export'
|
||||||
|
? ApiEndpoints.productsForExport(warehouseId)
|
||||||
|
: ApiEndpoints.products;
|
||||||
|
|
||||||
|
// Make API call to get products
|
||||||
|
final response = await apiClient.get(endpoint);
|
||||||
|
|
||||||
// Parse the API response using ApiResponse wrapper
|
// Parse the API response using ApiResponse wrapper
|
||||||
final apiResponse = ApiResponse.fromJson(
|
final apiResponse = ApiResponse.fromJson(
|
||||||
@@ -121,4 +135,47 @@ class ProductsRemoteDataSourceImpl implements ProductsRemoteDataSource {
|
|||||||
throw ServerException('Failed to get product stages: ${e.toString()}');
|
throw ServerException('Failed to get product stages: ${e.toString()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> createProductWarehouse(
|
||||||
|
CreateProductWarehouseRequest request) async {
|
||||||
|
try {
|
||||||
|
// The API expects an array of requests
|
||||||
|
final requestData = [request.toJson()];
|
||||||
|
|
||||||
|
// Make API call to create product warehouse
|
||||||
|
final response = await apiClient.post(
|
||||||
|
ApiEndpoints.createProductWarehouse,
|
||||||
|
data: requestData,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse the API response
|
||||||
|
final apiResponse = ApiResponse.fromJson(
|
||||||
|
response.data as Map<String, dynamic>,
|
||||||
|
(json) => json, // We don't need to parse the response value
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if the API call was successful
|
||||||
|
if (!apiResponse.isSuccess) {
|
||||||
|
// Throw exception with error message from API
|
||||||
|
throw ServerException(
|
||||||
|
apiResponse.errors.isNotEmpty
|
||||||
|
? apiResponse.errors.first
|
||||||
|
: 'Failed to create product warehouse entry',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} 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 create product warehouse entry: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/// Request model for creating product warehouse (import/export)
|
||||||
|
class CreateProductWarehouseRequest {
|
||||||
|
final int typeId;
|
||||||
|
final int productId;
|
||||||
|
final int stageId;
|
||||||
|
final int? orderId;
|
||||||
|
final String recordDate;
|
||||||
|
final double passedQuantityWeight;
|
||||||
|
final int passedQuantity;
|
||||||
|
final double issuedQuantityWeight;
|
||||||
|
final int issuedQuantity;
|
||||||
|
final int responsibleUserId;
|
||||||
|
final String description;
|
||||||
|
final String productName;
|
||||||
|
final String productCode;
|
||||||
|
final double stockPassedQuantityWeight;
|
||||||
|
final int stockPassedQuantity;
|
||||||
|
final int stockIssuedQuantity;
|
||||||
|
final double stockIssuedQuantityWeight;
|
||||||
|
final int receiverUserId;
|
||||||
|
final int actionTypeId;
|
||||||
|
final int wareHouseId;
|
||||||
|
final int productStageId;
|
||||||
|
final bool isConfirm;
|
||||||
|
|
||||||
|
CreateProductWarehouseRequest({
|
||||||
|
required this.typeId,
|
||||||
|
required this.productId,
|
||||||
|
required this.stageId,
|
||||||
|
this.orderId,
|
||||||
|
required this.recordDate,
|
||||||
|
required this.passedQuantityWeight,
|
||||||
|
required this.passedQuantity,
|
||||||
|
required this.issuedQuantityWeight,
|
||||||
|
required this.issuedQuantity,
|
||||||
|
required this.responsibleUserId,
|
||||||
|
this.description = '',
|
||||||
|
required this.productName,
|
||||||
|
required this.productCode,
|
||||||
|
this.stockPassedQuantityWeight = 0.0,
|
||||||
|
this.stockPassedQuantity = 0,
|
||||||
|
this.stockIssuedQuantity = 0,
|
||||||
|
this.stockIssuedQuantityWeight = 0.0,
|
||||||
|
required this.receiverUserId,
|
||||||
|
required this.actionTypeId,
|
||||||
|
required this.wareHouseId,
|
||||||
|
required this.productStageId,
|
||||||
|
this.isConfirm = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'TypeId': typeId,
|
||||||
|
'ProductId': productId,
|
||||||
|
'StageId': stageId,
|
||||||
|
'OrderId': orderId,
|
||||||
|
'RecordDate': recordDate,
|
||||||
|
'PassedQuantityWeight': passedQuantityWeight,
|
||||||
|
'PassedQuantity': passedQuantity,
|
||||||
|
'IssuedQuantityWeight': issuedQuantityWeight,
|
||||||
|
'IssuedQuantity': issuedQuantity,
|
||||||
|
'ResponsibleUserId': responsibleUserId,
|
||||||
|
'Description': description,
|
||||||
|
'ProductName': productName,
|
||||||
|
'ProductCode': productCode,
|
||||||
|
'StockPassedQuantityWeight': stockPassedQuantityWeight,
|
||||||
|
'StockPassedQuantity': stockPassedQuantity,
|
||||||
|
'StockIssuedQuantity': stockIssuedQuantity,
|
||||||
|
'StockIssuedQuantityWeight': stockIssuedQuantityWeight,
|
||||||
|
'ReceiverUserId': receiverUserId,
|
||||||
|
'ActionTypeId': actionTypeId,
|
||||||
|
'WareHouseId': wareHouseId,
|
||||||
|
'ProductStageId': productStageId,
|
||||||
|
'IsConfirm': isConfirm,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,9 @@ class ProductStageModel extends ProductStageEntity {
|
|||||||
required super.passedQuantityWeight,
|
required super.passedQuantityWeight,
|
||||||
required super.stageName,
|
required super.stageName,
|
||||||
required super.createdDate,
|
required super.createdDate,
|
||||||
|
super.productName,
|
||||||
|
super.productCode,
|
||||||
|
super.stageId,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Create ProductStageModel from JSON
|
/// Create ProductStageModel from JSON
|
||||||
@@ -27,6 +30,9 @@ class ProductStageModel extends ProductStageEntity {
|
|||||||
passedQuantityWeight: (json['PassedQuantityWeight'] as num).toDouble(),
|
passedQuantityWeight: (json['PassedQuantityWeight'] as num).toDouble(),
|
||||||
stageName: json['StageName'] as String?,
|
stageName: json['StageName'] as String?,
|
||||||
createdDate: json['CreatedDate'] as String,
|
createdDate: json['CreatedDate'] as String,
|
||||||
|
productName: json['ProductName'] as String? ?? '',
|
||||||
|
productCode: json['ProductCode'] as String? ?? '',
|
||||||
|
stageId: json['StageId'] as int?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +48,9 @@ class ProductStageModel extends ProductStageEntity {
|
|||||||
'PassedQuantityWeight': passedQuantityWeight,
|
'PassedQuantityWeight': passedQuantityWeight,
|
||||||
'StageName': stageName,
|
'StageName': stageName,
|
||||||
'CreatedDate': createdDate,
|
'CreatedDate': createdDate,
|
||||||
|
'ProductName': productName,
|
||||||
|
'ProductCode': productCode,
|
||||||
|
'StageId': stageId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +66,9 @@ class ProductStageModel extends ProductStageEntity {
|
|||||||
passedQuantityWeight: passedQuantityWeight,
|
passedQuantityWeight: passedQuantityWeight,
|
||||||
stageName: stageName,
|
stageName: stageName,
|
||||||
createdDate: createdDate,
|
createdDate: createdDate,
|
||||||
|
productName: productName,
|
||||||
|
productCode: productCode,
|
||||||
|
stageId: stageId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import '../../domain/entities/product_entity.dart';
|
|||||||
import '../../domain/entities/product_stage_entity.dart';
|
import '../../domain/entities/product_stage_entity.dart';
|
||||||
import '../../domain/repositories/products_repository.dart';
|
import '../../domain/repositories/products_repository.dart';
|
||||||
import '../datasources/products_remote_datasource.dart';
|
import '../datasources/products_remote_datasource.dart';
|
||||||
|
import '../models/create_product_warehouse_request.dart';
|
||||||
import '../models/product_detail_request_model.dart';
|
import '../models/product_detail_request_model.dart';
|
||||||
|
|
||||||
/// Implementation of ProductsRepository
|
/// Implementation of ProductsRepository
|
||||||
@@ -65,4 +66,26 @@ class ProductsRepositoryImpl implements ProductsRepository {
|
|||||||
return Left(ServerFailure('Unexpected error: ${e.toString()}'));
|
return Left(ServerFailure('Unexpected error: ${e.toString()}'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> createProductWarehouse(
|
||||||
|
CreateProductWarehouseRequest request,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
// Call remote data source to create product warehouse
|
||||||
|
await remoteDataSource.createProductWarehouse(request);
|
||||||
|
|
||||||
|
// Return success
|
||||||
|
return const Right(null);
|
||||||
|
} 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()}'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ class ProductStageEntity extends Equatable {
|
|||||||
final double passedQuantityWeight;
|
final double passedQuantityWeight;
|
||||||
final String? stageName;
|
final String? stageName;
|
||||||
final String createdDate;
|
final String createdDate;
|
||||||
|
final String productName;
|
||||||
|
final String productCode;
|
||||||
|
final int? stageId;
|
||||||
|
|
||||||
const ProductStageEntity({
|
const ProductStageEntity({
|
||||||
required this.productId,
|
required this.productId,
|
||||||
@@ -23,6 +26,9 @@ class ProductStageEntity extends Equatable {
|
|||||||
required this.passedQuantityWeight,
|
required this.passedQuantityWeight,
|
||||||
required this.stageName,
|
required this.stageName,
|
||||||
required this.createdDate,
|
required this.createdDate,
|
||||||
|
this.productName = '',
|
||||||
|
this.productCode = '',
|
||||||
|
this.stageId,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Get display name for the stage
|
/// Get display name for the stage
|
||||||
@@ -49,6 +55,9 @@ class ProductStageEntity extends Equatable {
|
|||||||
passedQuantityWeight,
|
passedQuantityWeight,
|
||||||
stageName,
|
stageName,
|
||||||
createdDate,
|
createdDate,
|
||||||
|
productName,
|
||||||
|
productCode,
|
||||||
|
stageId,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import '../../../../core/errors/failures.dart';
|
import '../../../../core/errors/failures.dart';
|
||||||
|
import '../../data/models/create_product_warehouse_request.dart';
|
||||||
import '../entities/product_entity.dart';
|
import '../entities/product_entity.dart';
|
||||||
import '../entities/product_stage_entity.dart';
|
import '../entities/product_stage_entity.dart';
|
||||||
|
|
||||||
@@ -27,4 +28,13 @@ abstract class ProductsRepository {
|
|||||||
int warehouseId,
|
int warehouseId,
|
||||||
int productId,
|
int productId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Create product warehouse entry (import/export operation)
|
||||||
|
///
|
||||||
|
/// [request] - Request containing all product warehouse details
|
||||||
|
///
|
||||||
|
/// Returns Either<Failure, void>
|
||||||
|
Future<Either<Failure, void>> createProductWarehouse(
|
||||||
|
CreateProductWarehouseRequest request,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../../core/di/providers.dart';
|
import '../../../../core/di/providers.dart';
|
||||||
|
import '../../../users/domain/entities/user_entity.dart';
|
||||||
|
import '../../data/models/create_product_warehouse_request.dart';
|
||||||
import '../../domain/entities/product_stage_entity.dart';
|
import '../../domain/entities/product_stage_entity.dart';
|
||||||
|
|
||||||
/// Product detail page
|
/// Product detail page
|
||||||
@@ -12,6 +14,7 @@ class ProductDetailPage extends ConsumerStatefulWidget {
|
|||||||
final int productId;
|
final int productId;
|
||||||
final String warehouseName;
|
final String warehouseName;
|
||||||
final int? stageId;
|
final int? stageId;
|
||||||
|
final String operationType;
|
||||||
|
|
||||||
const ProductDetailPage({
|
const ProductDetailPage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -19,6 +22,7 @@ class ProductDetailPage extends ConsumerStatefulWidget {
|
|||||||
required this.productId,
|
required this.productId,
|
||||||
required this.warehouseName,
|
required this.warehouseName,
|
||||||
this.stageId,
|
this.stageId,
|
||||||
|
this.operationType = 'import',
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,13 +32,26 @@ class ProductDetailPage extends ConsumerStatefulWidget {
|
|||||||
class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||||
late String _providerKey;
|
late String _providerKey;
|
||||||
|
|
||||||
|
// Text editing controllers for quantity fields
|
||||||
|
final TextEditingController _passedQuantityController = TextEditingController();
|
||||||
|
final TextEditingController _passedWeightController = TextEditingController();
|
||||||
|
final TextEditingController _issuedQuantityController = TextEditingController();
|
||||||
|
final TextEditingController _issuedWeightController = TextEditingController();
|
||||||
|
|
||||||
|
// Selected users for dropdowns
|
||||||
|
UserEntity? _selectedWarehouseUser;
|
||||||
|
UserEntity? _selectedEmployee;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_providerKey = '${widget.warehouseId}_${widget.productId}';
|
_providerKey = '${widget.warehouseId}_${widget.productId}';
|
||||||
|
|
||||||
// Load product stages when page is initialized
|
// Load product stages and users when page is initialized
|
||||||
Future.microtask(() async {
|
Future.microtask(() async {
|
||||||
|
// Load users from Hive (no API call)
|
||||||
|
await ref.read(usersProvider.notifier).getUsers();
|
||||||
|
|
||||||
await ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail(
|
await ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail(
|
||||||
widget.warehouseId,
|
widget.warehouseId,
|
||||||
widget.productId,
|
widget.productId,
|
||||||
@@ -53,6 +70,15 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_passedQuantityController.dispose();
|
||||||
|
_passedWeightController.dispose();
|
||||||
|
_issuedQuantityController.dispose();
|
||||||
|
_issuedWeightController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onRefresh() async {
|
Future<void> _onRefresh() async {
|
||||||
await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
|
await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
|
||||||
widget.warehouseId,
|
widget.warehouseId,
|
||||||
@@ -60,6 +86,17 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearControllers() {
|
||||||
|
_passedQuantityController.clear();
|
||||||
|
_passedWeightController.clear();
|
||||||
|
_issuedQuantityController.clear();
|
||||||
|
_issuedWeightController.clear();
|
||||||
|
setState(() {
|
||||||
|
_selectedWarehouseUser = null;
|
||||||
|
_selectedEmployee = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -73,17 +110,23 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
final error = productDetailState.error;
|
final error = productDetailState.error;
|
||||||
final selectedIndex = productDetailState.selectedStageIndex;
|
final selectedIndex = productDetailState.selectedStageIndex;
|
||||||
|
|
||||||
|
// Get product name from stages if available
|
||||||
|
final productName = stages.isNotEmpty ? stages.first.productName : 'Product';
|
||||||
|
|
||||||
|
// Capitalize first letter of operation type
|
||||||
|
final operationTitle = widget.operationType == 'import' ? 'Import' : 'Export';
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Product Stages',
|
operationTitle,
|
||||||
style: textTheme.titleMedium,
|
style: textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
widget.warehouseName,
|
productName,
|
||||||
style: textTheme.bodySmall?.copyWith(
|
style: textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -333,33 +376,11 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
// Stage header
|
// Stage header
|
||||||
_buildStageHeader(stageToShow, theme),
|
_buildStageHeader(stageToShow, theme),
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Quantity information
|
|
||||||
_buildSectionCard(
|
|
||||||
theme: theme,
|
|
||||||
title: 'Quantities',
|
|
||||||
icon: Icons.inventory_outlined,
|
|
||||||
children: [
|
|
||||||
_buildInfoRow('Passed Quantity', '${stageToShow.passedQuantity}'),
|
|
||||||
_buildInfoRow(
|
|
||||||
'Passed Weight',
|
|
||||||
'${stageToShow.passedQuantityWeight.toStringAsFixed(2)} kg',
|
|
||||||
),
|
|
||||||
const Divider(height: 24),
|
|
||||||
_buildInfoRow('Issued Quantity', '${stageToShow.issuedQuantity}'),
|
|
||||||
_buildInfoRow(
|
|
||||||
'Issued Weight',
|
|
||||||
'${stageToShow.issuedQuantityWeight.toStringAsFixed(2)} kg',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Stage information
|
|
||||||
_buildSectionCard(
|
_buildSectionCard(
|
||||||
theme: theme,
|
theme: theme,
|
||||||
title: 'Stage Information',
|
title: 'Stage Information',
|
||||||
@@ -373,10 +394,115 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
_buildInfoRow('Stage Name', stageToShow.displayName),
|
_buildInfoRow('Stage Name', stageToShow.displayName),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Status indicators
|
// Current Quantity information
|
||||||
_buildStatusCards(stageToShow, theme),
|
_buildSectionCard(
|
||||||
|
theme: theme,
|
||||||
|
title: 'Current Quantities',
|
||||||
|
icon: Icons.info_outlined,
|
||||||
|
children: [
|
||||||
|
_buildInfoRow('Passed Quantity', '${stageToShow.passedQuantity}'),
|
||||||
|
_buildInfoRow(
|
||||||
|
'Passed Weight',
|
||||||
|
'${stageToShow.passedQuantityWeight.toStringAsFixed(2)} kg',
|
||||||
|
),
|
||||||
|
_buildInfoRow('Issued Quantity', '${stageToShow.issuedQuantity}'),
|
||||||
|
_buildInfoRow(
|
||||||
|
'Issued Weight',
|
||||||
|
'${stageToShow.issuedQuantityWeight.toStringAsFixed(2)} kg',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Add New Quantities section
|
||||||
|
_buildSectionCard(
|
||||||
|
theme: theme,
|
||||||
|
title: 'Add New Quantities',
|
||||||
|
icon: Icons.add_circle_outline,
|
||||||
|
children: [
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Passed Quantity',
|
||||||
|
controller: _passedQuantityController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Passed Weight (kg)',
|
||||||
|
controller: _passedWeightController,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Issued Quantity',
|
||||||
|
controller: _issuedQuantityController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Issued Weight (kg)',
|
||||||
|
controller: _issuedWeightController,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildSectionCard(theme: theme, title: "Nhân viên", icon: Icons.people, children: [
|
||||||
|
// Warehouse User Dropdown
|
||||||
|
_buildUserDropdown(
|
||||||
|
label: 'Warehouse User',
|
||||||
|
value: _selectedWarehouseUser,
|
||||||
|
users: ref.watch(usersListProvider)
|
||||||
|
.where((user) => user.isWareHouseUser)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (user) {
|
||||||
|
setState(() {
|
||||||
|
_selectedWarehouseUser = user;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
// All Employees Dropdown
|
||||||
|
_buildUserDropdown(
|
||||||
|
label: 'Employee',
|
||||||
|
value: _selectedEmployee,
|
||||||
|
users: ref.watch(usersListProvider),
|
||||||
|
onChanged: (user) {
|
||||||
|
setState(() {
|
||||||
|
_selectedEmployee = user;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () => _printQuantities(stageToShow),
|
||||||
|
icon: const Icon(Icons.print),
|
||||||
|
label: const Text('Print'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: () => _addNewQuantities(stageToShow),
|
||||||
|
icon: const Icon(Icons.save),
|
||||||
|
label: const Text('Save'),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -387,6 +513,146 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _printQuantities(ProductStageEntity stage) {
|
||||||
|
// TODO: Implement print functionality
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Print functionality coming soon'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _addNewQuantities(ProductStageEntity stage) async {
|
||||||
|
// Parse the values from text fields
|
||||||
|
final passedQuantity = int.tryParse(_passedQuantityController.text) ?? 0;
|
||||||
|
final passedWeight = double.tryParse(_passedWeightController.text) ?? 0.0;
|
||||||
|
final issuedQuantity = int.tryParse(_issuedQuantityController.text) ?? 0;
|
||||||
|
final issuedWeight = double.tryParse(_issuedWeightController.text) ?? 0.0;
|
||||||
|
|
||||||
|
// Validate that at least one field has a value
|
||||||
|
if (passedQuantity == 0 && passedWeight == 0.0 &&
|
||||||
|
issuedQuantity == 0 && issuedWeight == 0.0) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Please enter at least one quantity or weight value'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that both users are selected
|
||||||
|
if (_selectedEmployee == null || _selectedWarehouseUser == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Please select both Employee and Warehouse User'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading dialog
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Determine actionTypeId based on operation type
|
||||||
|
// 4 = Import, 5 = Export
|
||||||
|
final typeId = widget.operationType == 'import' ? 4 : 5;
|
||||||
|
final actionTypeId = widget.operationType == 'import' ? 1 : 2;
|
||||||
|
|
||||||
|
// Create request with all required fields
|
||||||
|
final request = CreateProductWarehouseRequest(
|
||||||
|
typeId: typeId, // Import type
|
||||||
|
productId: stage.productId,
|
||||||
|
stageId: stage.stageId ?? 0,
|
||||||
|
orderId: null,
|
||||||
|
recordDate: DateTime.now().toIso8601String(),
|
||||||
|
passedQuantityWeight: passedWeight,
|
||||||
|
passedQuantity: passedQuantity,
|
||||||
|
issuedQuantityWeight: issuedWeight,
|
||||||
|
issuedQuantity: issuedQuantity,
|
||||||
|
responsibleUserId: _selectedEmployee!.id,
|
||||||
|
description: '',
|
||||||
|
productName: stage.productName,
|
||||||
|
productCode: stage.productCode,
|
||||||
|
stockPassedQuantityWeight: stage.passedQuantityWeight,
|
||||||
|
stockPassedQuantity: stage.passedQuantity,
|
||||||
|
stockIssuedQuantity: stage.issuedQuantity,
|
||||||
|
stockIssuedQuantityWeight: stage.issuedQuantityWeight,
|
||||||
|
receiverUserId: _selectedWarehouseUser!.id,
|
||||||
|
actionTypeId: actionTypeId,
|
||||||
|
wareHouseId: widget.warehouseId,
|
||||||
|
productStageId: stage.productStageId ?? 0,
|
||||||
|
isConfirm: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call the repository to create product warehouse entry
|
||||||
|
final repository = ref.read(productsRepositoryProvider);
|
||||||
|
final result = await repository.createProductWarehouse(request);
|
||||||
|
|
||||||
|
// Dismiss loading dialog
|
||||||
|
if (mounted) Navigator.of(context).pop();
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) {
|
||||||
|
// Show error message
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to add quantities: ${failure.message}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(_) {
|
||||||
|
// Success - show success message
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Quantities added successfully!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear the text fields after successful add
|
||||||
|
_clearControllers();
|
||||||
|
|
||||||
|
// Refresh the product detail to show updated quantities
|
||||||
|
ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
|
||||||
|
widget.warehouseId,
|
||||||
|
widget.productId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Dismiss loading dialog
|
||||||
|
if (mounted) Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Error: ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStageHeader(ProductStageEntity stage, ThemeData theme) {
|
Widget _buildStageHeader(ProductStageEntity stage, ThemeData theme) {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
@@ -445,6 +711,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 12,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -472,7 +739,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
|
|
||||||
Widget _buildInfoRow(String label, String value) {
|
Widget _buildInfoRow(String label, String value) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -501,6 +768,159 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTextField({
|
||||||
|
required String label,
|
||||||
|
required TextEditingController controller,
|
||||||
|
required TextInputType keyboardType,
|
||||||
|
required ThemeData theme,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: theme.colorScheme.surface,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => _incrementValue(controller, 0.1),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
),
|
||||||
|
child: const Text('+0.1'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => _incrementValue(controller, 0.5),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
),
|
||||||
|
child: const Text('+0.5'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => _incrementValue(controller, 1),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
),
|
||||||
|
child: const Text('+1'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => controller.clear(),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
foregroundColor: theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
child: const Text('C'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUserDropdown({
|
||||||
|
required String label,
|
||||||
|
required UserEntity? value,
|
||||||
|
required List<UserEntity> users,
|
||||||
|
required Function(UserEntity?) onChanged,
|
||||||
|
required ThemeData theme,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<UserEntity>(
|
||||||
|
value: value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: theme.colorScheme.surface,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
hint: Text('Select $label'),
|
||||||
|
items: users.map((user) {
|
||||||
|
return DropdownMenuItem<UserEntity>(
|
||||||
|
value: user,
|
||||||
|
child: Text(
|
||||||
|
user.name.isNotEmpty ? '${user.name} ${user.firstName}' : user.email,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: onChanged,
|
||||||
|
isExpanded: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _incrementValue(TextEditingController controller, double increment) {
|
||||||
|
final currentValue = double.tryParse(controller.text) ?? 0.0;
|
||||||
|
final newValue = currentValue + increment;
|
||||||
|
|
||||||
|
// Format the value based on whether it's a whole number or has decimals
|
||||||
|
if (newValue == newValue.toInt()) {
|
||||||
|
controller.text = newValue.toInt().toString();
|
||||||
|
} else {
|
||||||
|
controller.text = newValue.toStringAsFixed(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStatusCards(ProductStageEntity stage, ThemeData theme) {
|
Widget _buildStatusCards(ProductStageEntity stage, ThemeData theme) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import '../../../../core/router/app_router.dart';
|
|||||||
import '../widgets/product_list_item.dart';
|
import '../widgets/product_list_item.dart';
|
||||||
|
|
||||||
/// Products list page
|
/// Products list page
|
||||||
/// Displays products for a specific warehouse and operation type
|
/// Displays products for a specific warehouse with import/export tabs
|
||||||
class ProductsPage extends ConsumerStatefulWidget {
|
class ProductsPage extends ConsumerStatefulWidget {
|
||||||
final int warehouseId;
|
final int warehouseId;
|
||||||
final String warehouseName;
|
final String warehouseName;
|
||||||
@@ -24,20 +24,61 @@ class ProductsPage extends ConsumerStatefulWidget {
|
|||||||
ConsumerState<ProductsPage> createState() => _ProductsPageState();
|
ConsumerState<ProductsPage> createState() => _ProductsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProductsPageState extends ConsumerState<ProductsPage> {
|
class _ProductsPageState extends ConsumerState<ProductsPage>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
String _currentOperationType = 'import';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
// Initialize tab controller
|
||||||
|
_tabController = TabController(
|
||||||
|
length: 2,
|
||||||
|
vsync: this,
|
||||||
|
initialIndex: widget.operationType == 'export' ? 1 : 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
_currentOperationType = widget.operationType;
|
||||||
|
|
||||||
|
// Listen to tab changes
|
||||||
|
_tabController.addListener(() {
|
||||||
|
if (_tabController.indexIsChanging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newOperationType = _tabController.index == 0 ? 'import' : 'export';
|
||||||
|
if (_currentOperationType != newOperationType) {
|
||||||
|
setState(() {
|
||||||
|
_currentOperationType = newOperationType;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load products for new operation type
|
||||||
|
ref.read(productsProvider.notifier).loadProducts(
|
||||||
|
widget.warehouseId,
|
||||||
|
widget.warehouseName,
|
||||||
|
_currentOperationType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Load products when page is initialized
|
// Load products when page is initialized
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
ref.read(productsProvider.notifier).loadProducts(
|
ref.read(productsProvider.notifier).loadProducts(
|
||||||
widget.warehouseId,
|
widget.warehouseId,
|
||||||
widget.warehouseName,
|
widget.warehouseName,
|
||||||
widget.operationType,
|
_currentOperationType,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onRefresh() async {
|
Future<void> _onRefresh() async {
|
||||||
await ref.read(productsProvider.notifier).refreshProducts();
|
await ref.read(productsProvider.notifier).refreshProducts();
|
||||||
}
|
}
|
||||||
@@ -173,6 +214,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
|
|||||||
warehouseId: widget.warehouseId,
|
warehouseId: widget.warehouseId,
|
||||||
productId: productId,
|
productId: productId,
|
||||||
warehouseName: widget.warehouseName,
|
warehouseName: widget.warehouseName,
|
||||||
|
operationType: widget.operationType,
|
||||||
stageId: stageId,
|
stageId: stageId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -194,7 +236,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Products (${_getOperationTypeDisplay()})',
|
'Products',
|
||||||
style: textTheme.titleMedium,
|
style: textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -212,6 +254,19 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
|
|||||||
tooltip: 'Refresh',
|
tooltip: 'Refresh',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: const [
|
||||||
|
Tab(
|
||||||
|
icon: Icon(Icons.arrow_downward),
|
||||||
|
text: 'Import',
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
icon: Icon(Icons.arrow_upward),
|
||||||
|
text: 'Export',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: _buildBody(
|
body: _buildBody(
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
@@ -253,10 +308,10 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
widget.operationType == 'import'
|
_currentOperationType == 'import'
|
||||||
? Icons.arrow_downward
|
? Icons.arrow_downward
|
||||||
: Icons.arrow_upward,
|
: Icons.arrow_upward,
|
||||||
color: widget.operationType == 'import'
|
color: _currentOperationType == 'import'
|
||||||
? Colors.green
|
? Colors.green
|
||||||
: Colors.orange,
|
: Colors.orange,
|
||||||
),
|
),
|
||||||
@@ -266,7 +321,9 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_getOperationTypeDisplay(),
|
_currentOperationType == 'import'
|
||||||
|
? 'Import Products'
|
||||||
|
: 'Export Products',
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -410,6 +467,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
|
|||||||
warehouseId: widget.warehouseId,
|
warehouseId: widget.warehouseId,
|
||||||
productId: product.id,
|
productId: product.id,
|
||||||
warehouseName: widget.warehouseName,
|
warehouseName: widget.warehouseName,
|
||||||
|
operationType: widget.operationType,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -417,11 +475,4 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get display text for operation type
|
|
||||||
String _getOperationTypeDisplay() {
|
|
||||||
return widget.operationType == 'import'
|
|
||||||
? 'Import Products'
|
|
||||||
: 'Export Products';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:hive_ce/hive.dart';
|
||||||
|
|
||||||
|
import '../models/user_model.dart';
|
||||||
|
|
||||||
|
/// Abstract interface for users local data source
|
||||||
|
abstract class UsersLocalDataSource {
|
||||||
|
/// Get all users from local storage
|
||||||
|
Future<List<UserModel>> getUsers();
|
||||||
|
|
||||||
|
/// Save users to local storage
|
||||||
|
Future<void> saveUsers(List<UserModel> users);
|
||||||
|
|
||||||
|
/// Clear all users from local storage
|
||||||
|
Future<void> clearUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of UsersLocalDataSource using Hive
|
||||||
|
class UsersLocalDataSourceImpl implements UsersLocalDataSource {
|
||||||
|
static const String _boxName = 'users';
|
||||||
|
|
||||||
|
Future<Box<UserModel>> get _box async {
|
||||||
|
if (!Hive.isBoxOpen(_boxName)) {
|
||||||
|
return await Hive.openBox<UserModel>(_boxName);
|
||||||
|
}
|
||||||
|
return Hive.box<UserModel>(_boxName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<UserModel>> getUsers() async {
|
||||||
|
try {
|
||||||
|
final box = await _box;
|
||||||
|
return box.values.toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to get users from local storage: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveUsers(List<UserModel> users) async {
|
||||||
|
try {
|
||||||
|
final box = await _box;
|
||||||
|
await box.clear();
|
||||||
|
|
||||||
|
// Save users with their ID as key
|
||||||
|
for (final user in users) {
|
||||||
|
await box.put(user.id, user);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to save users to local storage: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearUsers() async {
|
||||||
|
try {
|
||||||
|
final box = await _box;
|
||||||
|
await box.clear();
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to clear users from local storage: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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/user_model.dart';
|
||||||
|
|
||||||
|
/// Abstract interface for users remote data source
|
||||||
|
abstract class UsersRemoteDataSource {
|
||||||
|
/// Fetch all users from the API
|
||||||
|
Future<List<UserModel>> getUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of UsersRemoteDataSource using ApiClient
|
||||||
|
class UsersRemoteDataSourceImpl implements UsersRemoteDataSource {
|
||||||
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
UsersRemoteDataSourceImpl(this.apiClient);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<UserModel>> getUsers() async {
|
||||||
|
try {
|
||||||
|
// Make API call to get all users
|
||||||
|
final response = await apiClient.get(ApiEndpoints.users);
|
||||||
|
|
||||||
|
// Parse the API response using ApiResponse wrapper
|
||||||
|
final apiResponse = ApiResponse.fromJson(
|
||||||
|
response.data as Map<String, dynamic>,
|
||||||
|
(json) => (json as List)
|
||||||
|
.map((e) => UserModel.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 users',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} 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 users: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
lib/features/users/data/models/user_model.dart
Normal file
163
lib/features/users/data/models/user_model.dart
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import 'package:hive_ce/hive.dart';
|
||||||
|
|
||||||
|
import '../../domain/entities/user_entity.dart';
|
||||||
|
|
||||||
|
part 'user_model.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: 1)
|
||||||
|
class UserModel extends UserEntity {
|
||||||
|
@HiveField(0)
|
||||||
|
@override
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
@HiveField(1)
|
||||||
|
@override
|
||||||
|
final String firstName;
|
||||||
|
|
||||||
|
@HiveField(2)
|
||||||
|
@override
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
@HiveField(3)
|
||||||
|
@override
|
||||||
|
final String? plateNumber;
|
||||||
|
|
||||||
|
@HiveField(4)
|
||||||
|
@override
|
||||||
|
final String email;
|
||||||
|
|
||||||
|
@HiveField(5)
|
||||||
|
@override
|
||||||
|
final String phone;
|
||||||
|
|
||||||
|
@HiveField(6)
|
||||||
|
@override
|
||||||
|
final bool isParent;
|
||||||
|
|
||||||
|
@HiveField(7)
|
||||||
|
@override
|
||||||
|
final String fullName;
|
||||||
|
|
||||||
|
@HiveField(8)
|
||||||
|
@override
|
||||||
|
final String fullNameEmail;
|
||||||
|
|
||||||
|
@HiveField(9)
|
||||||
|
@override
|
||||||
|
final String? referralCode;
|
||||||
|
|
||||||
|
@HiveField(10)
|
||||||
|
@override
|
||||||
|
final String? avatar;
|
||||||
|
|
||||||
|
@HiveField(11)
|
||||||
|
@override
|
||||||
|
final int departmentId;
|
||||||
|
|
||||||
|
@HiveField(12)
|
||||||
|
@override
|
||||||
|
final bool isWareHouseUser;
|
||||||
|
|
||||||
|
@HiveField(13)
|
||||||
|
@override
|
||||||
|
final int? wareHouseId;
|
||||||
|
|
||||||
|
@HiveField(14)
|
||||||
|
@override
|
||||||
|
final int roleId;
|
||||||
|
|
||||||
|
const UserModel({
|
||||||
|
required this.id,
|
||||||
|
required this.firstName,
|
||||||
|
required this.name,
|
||||||
|
this.plateNumber,
|
||||||
|
required this.email,
|
||||||
|
required this.phone,
|
||||||
|
this.isParent = false,
|
||||||
|
required this.fullName,
|
||||||
|
required this.fullNameEmail,
|
||||||
|
this.referralCode,
|
||||||
|
this.avatar,
|
||||||
|
required this.departmentId,
|
||||||
|
this.isWareHouseUser = false,
|
||||||
|
this.wareHouseId,
|
||||||
|
required this.roleId,
|
||||||
|
}) : super(
|
||||||
|
id: id,
|
||||||
|
firstName: firstName,
|
||||||
|
name: name,
|
||||||
|
plateNumber: plateNumber,
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
isParent: isParent,
|
||||||
|
fullName: fullName,
|
||||||
|
fullNameEmail: fullNameEmail,
|
||||||
|
referralCode: referralCode,
|
||||||
|
avatar: avatar,
|
||||||
|
departmentId: departmentId,
|
||||||
|
isWareHouseUser: isWareHouseUser,
|
||||||
|
wareHouseId: wareHouseId,
|
||||||
|
roleId: roleId,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory UserModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return UserModel(
|
||||||
|
id: json['Id'] ?? 0,
|
||||||
|
firstName: json['FirstName'] ?? '',
|
||||||
|
name: json['Name'] ?? '',
|
||||||
|
plateNumber: json['PlateNumber'],
|
||||||
|
email: json['Email'] ?? '',
|
||||||
|
phone: json['Phone'] ?? '',
|
||||||
|
isParent: json['IsParent'] ?? false,
|
||||||
|
fullName: json['FullName'] ?? '',
|
||||||
|
fullNameEmail: json['FullNameEmail'] ?? '',
|
||||||
|
referralCode: json['ReferralCode'],
|
||||||
|
avatar: json['Avatar'],
|
||||||
|
departmentId: json['DepartmentId'] ?? 0,
|
||||||
|
isWareHouseUser: json['IsWareHouseUser'] ?? false,
|
||||||
|
wareHouseId: json['WareHouseId'],
|
||||||
|
roleId: json['RoleId'] ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'Id': id,
|
||||||
|
'FirstName': firstName,
|
||||||
|
'Name': name,
|
||||||
|
'PlateNumber': plateNumber,
|
||||||
|
'Email': email,
|
||||||
|
'Phone': phone,
|
||||||
|
'IsParent': isParent,
|
||||||
|
'FullName': fullName,
|
||||||
|
'FullNameEmail': fullNameEmail,
|
||||||
|
'ReferralCode': referralCode,
|
||||||
|
'Avatar': avatar,
|
||||||
|
'DepartmentId': departmentId,
|
||||||
|
'IsWareHouseUser': isWareHouseUser,
|
||||||
|
'WareHouseId': wareHouseId,
|
||||||
|
'RoleId': roleId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntity toEntity() {
|
||||||
|
return UserEntity(
|
||||||
|
id: id,
|
||||||
|
firstName: firstName,
|
||||||
|
name: name,
|
||||||
|
plateNumber: plateNumber,
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
isParent: isParent,
|
||||||
|
fullName: fullName,
|
||||||
|
fullNameEmail: fullNameEmail,
|
||||||
|
referralCode: referralCode,
|
||||||
|
avatar: avatar,
|
||||||
|
departmentId: departmentId,
|
||||||
|
isWareHouseUser: isWareHouseUser,
|
||||||
|
wareHouseId: wareHouseId,
|
||||||
|
roleId: roleId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
83
lib/features/users/data/models/user_model.g.dart
Normal file
83
lib/features/users/data/models/user_model.g.dart
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'user_model.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// TypeAdapterGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
class UserModelAdapter extends TypeAdapter<UserModel> {
|
||||||
|
@override
|
||||||
|
final typeId = 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
UserModel read(BinaryReader reader) {
|
||||||
|
final numOfFields = reader.readByte();
|
||||||
|
final fields = <int, dynamic>{
|
||||||
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
|
};
|
||||||
|
return UserModel(
|
||||||
|
id: (fields[0] as num).toInt(),
|
||||||
|
firstName: fields[1] as String,
|
||||||
|
name: fields[2] as String,
|
||||||
|
plateNumber: fields[3] as String?,
|
||||||
|
email: fields[4] as String,
|
||||||
|
phone: fields[5] as String,
|
||||||
|
isParent: fields[6] == null ? false : fields[6] as bool,
|
||||||
|
fullName: fields[7] as String,
|
||||||
|
fullNameEmail: fields[8] as String,
|
||||||
|
referralCode: fields[9] as String?,
|
||||||
|
avatar: fields[10] as String?,
|
||||||
|
departmentId: (fields[11] as num).toInt(),
|
||||||
|
isWareHouseUser: fields[12] == null ? false : fields[12] as bool,
|
||||||
|
wareHouseId: (fields[13] as num?)?.toInt(),
|
||||||
|
roleId: (fields[14] as num).toInt(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, UserModel obj) {
|
||||||
|
writer
|
||||||
|
..writeByte(15)
|
||||||
|
..writeByte(0)
|
||||||
|
..write(obj.id)
|
||||||
|
..writeByte(1)
|
||||||
|
..write(obj.firstName)
|
||||||
|
..writeByte(2)
|
||||||
|
..write(obj.name)
|
||||||
|
..writeByte(3)
|
||||||
|
..write(obj.plateNumber)
|
||||||
|
..writeByte(4)
|
||||||
|
..write(obj.email)
|
||||||
|
..writeByte(5)
|
||||||
|
..write(obj.phone)
|
||||||
|
..writeByte(6)
|
||||||
|
..write(obj.isParent)
|
||||||
|
..writeByte(7)
|
||||||
|
..write(obj.fullName)
|
||||||
|
..writeByte(8)
|
||||||
|
..write(obj.fullNameEmail)
|
||||||
|
..writeByte(9)
|
||||||
|
..write(obj.referralCode)
|
||||||
|
..writeByte(10)
|
||||||
|
..write(obj.avatar)
|
||||||
|
..writeByte(11)
|
||||||
|
..write(obj.departmentId)
|
||||||
|
..writeByte(12)
|
||||||
|
..write(obj.isWareHouseUser)
|
||||||
|
..writeByte(13)
|
||||||
|
..write(obj.wareHouseId)
|
||||||
|
..writeByte(14)
|
||||||
|
..write(obj.roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is UserModelAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
|
||||||
|
import '../../../../core/errors/exceptions.dart';
|
||||||
|
import '../../../../core/errors/failures.dart';
|
||||||
|
import '../../domain/entities/user_entity.dart';
|
||||||
|
import '../../domain/repositories/users_repository.dart';
|
||||||
|
import '../datasources/users_local_datasource.dart';
|
||||||
|
import '../datasources/users_remote_datasource.dart';
|
||||||
|
|
||||||
|
/// Implementation of UsersRepository
|
||||||
|
class UsersRepositoryImpl implements UsersRepository {
|
||||||
|
final UsersRemoteDataSource remoteDataSource;
|
||||||
|
final UsersLocalDataSource localDataSource;
|
||||||
|
|
||||||
|
UsersRepositoryImpl({
|
||||||
|
required this.remoteDataSource,
|
||||||
|
required this.localDataSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<UserEntity>>> getUsers() async {
|
||||||
|
try {
|
||||||
|
// Try to get users from local storage first
|
||||||
|
final localUsers = await localDataSource.getUsers();
|
||||||
|
|
||||||
|
if (localUsers.isNotEmpty) {
|
||||||
|
// Return local users if available
|
||||||
|
return Right(localUsers.map((model) => model.toEntity()).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no local users, fetch from API
|
||||||
|
return await syncUsers();
|
||||||
|
} catch (e) {
|
||||||
|
return Left(CacheFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<UserEntity>>> syncUsers() async {
|
||||||
|
try {
|
||||||
|
// Fetch users from API
|
||||||
|
final users = await remoteDataSource.getUsers();
|
||||||
|
|
||||||
|
// Save to local storage
|
||||||
|
await localDataSource.saveUsers(users);
|
||||||
|
|
||||||
|
// Return as entities
|
||||||
|
return Right(users.map((model) => model.toEntity()).toList());
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
return Left(ServerFailure(e.message));
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
return Left(NetworkFailure(e.message));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(ServerFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> clearUsers() async {
|
||||||
|
try {
|
||||||
|
await localDataSource.clearUsers();
|
||||||
|
return const Right(null);
|
||||||
|
} catch (e) {
|
||||||
|
return Left(CacheFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
lib/features/users/domain/entities/user_entity.dart
Normal file
78
lib/features/users/domain/entities/user_entity.dart
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// User entity representing a user in the system
|
||||||
|
class UserEntity extends Equatable {
|
||||||
|
final int id;
|
||||||
|
final String firstName;
|
||||||
|
final String name;
|
||||||
|
final String? plateNumber;
|
||||||
|
final String email;
|
||||||
|
final String phone;
|
||||||
|
final bool isParent;
|
||||||
|
final String fullName;
|
||||||
|
final String fullNameEmail;
|
||||||
|
final String? referralCode;
|
||||||
|
final String? avatar;
|
||||||
|
final int departmentId;
|
||||||
|
final bool isWareHouseUser;
|
||||||
|
final int? wareHouseId;
|
||||||
|
final int roleId;
|
||||||
|
|
||||||
|
const UserEntity({
|
||||||
|
required this.id,
|
||||||
|
required this.firstName,
|
||||||
|
required this.name,
|
||||||
|
this.plateNumber,
|
||||||
|
required this.email,
|
||||||
|
required this.phone,
|
||||||
|
this.isParent = false,
|
||||||
|
required this.fullName,
|
||||||
|
required this.fullNameEmail,
|
||||||
|
this.referralCode,
|
||||||
|
this.avatar,
|
||||||
|
required this.departmentId,
|
||||||
|
this.isWareHouseUser = false,
|
||||||
|
this.wareHouseId,
|
||||||
|
required this.roleId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
firstName,
|
||||||
|
name,
|
||||||
|
plateNumber,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
isParent,
|
||||||
|
fullName,
|
||||||
|
fullNameEmail,
|
||||||
|
referralCode,
|
||||||
|
avatar,
|
||||||
|
departmentId,
|
||||||
|
isWareHouseUser,
|
||||||
|
wareHouseId,
|
||||||
|
roleId,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Get display name
|
||||||
|
String get displayName {
|
||||||
|
if (fullName.isNotEmpty) return fullName;
|
||||||
|
if (name.isNotEmpty) return name;
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get initials from name (for avatar display)
|
||||||
|
String get initials {
|
||||||
|
if (firstName.isNotEmpty && name.isNotEmpty) {
|
||||||
|
return (firstName.substring(0, 1) + name.substring(0, 1)).toUpperCase();
|
||||||
|
}
|
||||||
|
final parts = fullName.trim().split(' ');
|
||||||
|
if (parts.isEmpty) return '?';
|
||||||
|
if (parts.length == 1) {
|
||||||
|
return parts[0].substring(0, 1).toUpperCase();
|
||||||
|
}
|
||||||
|
return (parts[0].substring(0, 1) + parts[parts.length - 1].substring(0, 1))
|
||||||
|
.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
lib/features/users/domain/repositories/users_repository.dart
Normal file
16
lib/features/users/domain/repositories/users_repository.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
|
||||||
|
import '../../../../core/errors/failures.dart';
|
||||||
|
import '../entities/user_entity.dart';
|
||||||
|
|
||||||
|
/// Abstract repository interface for users
|
||||||
|
abstract class UsersRepository {
|
||||||
|
/// Get all users (from local storage if available, otherwise from API)
|
||||||
|
Future<Either<Failure, List<UserEntity>>> getUsers();
|
||||||
|
|
||||||
|
/// Sync users from API and save to local storage
|
||||||
|
Future<Either<Failure, List<UserEntity>>> syncUsers();
|
||||||
|
|
||||||
|
/// Clear all users from local storage
|
||||||
|
Future<Either<Failure, void>> clearUsers();
|
||||||
|
}
|
||||||
18
lib/features/users/domain/usecases/get_users_usecase.dart
Normal file
18
lib/features/users/domain/usecases/get_users_usecase.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
|
||||||
|
import '../../../../core/errors/failures.dart';
|
||||||
|
import '../entities/user_entity.dart';
|
||||||
|
import '../repositories/users_repository.dart';
|
||||||
|
|
||||||
|
/// Use case for getting users
|
||||||
|
/// Follows local-first strategy: returns local users if available,
|
||||||
|
/// otherwise syncs from API
|
||||||
|
class GetUsersUseCase {
|
||||||
|
final UsersRepository repository;
|
||||||
|
|
||||||
|
GetUsersUseCase(this.repository);
|
||||||
|
|
||||||
|
Future<Either<Failure, List<UserEntity>>> call() async {
|
||||||
|
return await repository.getUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
lib/features/users/domain/usecases/sync_users_usecase.dart
Normal file
17
lib/features/users/domain/usecases/sync_users_usecase.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
|
||||||
|
import '../../../../core/errors/failures.dart';
|
||||||
|
import '../entities/user_entity.dart';
|
||||||
|
import '../repositories/users_repository.dart';
|
||||||
|
|
||||||
|
/// Use case for syncing users from API
|
||||||
|
/// Forces refresh from API and saves to local storage
|
||||||
|
class SyncUsersUseCase {
|
||||||
|
final UsersRepository repository;
|
||||||
|
|
||||||
|
SyncUsersUseCase(this.repository);
|
||||||
|
|
||||||
|
Future<Either<Failure, List<UserEntity>>> call() async {
|
||||||
|
return await repository.syncUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../domain/usecases/get_users_usecase.dart';
|
||||||
|
import '../../domain/usecases/sync_users_usecase.dart';
|
||||||
|
import 'users_state.dart';
|
||||||
|
|
||||||
|
/// State notifier for users
|
||||||
|
class UsersNotifier extends StateNotifier<UsersState> {
|
||||||
|
final GetUsersUseCase getUsersUseCase;
|
||||||
|
final SyncUsersUseCase syncUsersUseCase;
|
||||||
|
|
||||||
|
UsersNotifier({
|
||||||
|
required this.getUsersUseCase,
|
||||||
|
required this.syncUsersUseCase,
|
||||||
|
}) : super(const UsersState());
|
||||||
|
|
||||||
|
/// Get users from local storage (or API if not cached)
|
||||||
|
Future<void> getUsers() async {
|
||||||
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
|
||||||
|
final result = await getUsersUseCase();
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => state = state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
error: failure.message,
|
||||||
|
),
|
||||||
|
(users) => state = state.copyWith(
|
||||||
|
users: users,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sync users from API (force refresh)
|
||||||
|
Future<void> syncUsers() async {
|
||||||
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
|
||||||
|
final result = await syncUsersUseCase();
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => state = state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
error: failure.message,
|
||||||
|
),
|
||||||
|
(users) => state = state.copyWith(
|
||||||
|
users: users,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/features/users/presentation/providers/users_state.dart
Normal file
31
lib/features/users/presentation/providers/users_state.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
import '../../domain/entities/user_entity.dart';
|
||||||
|
|
||||||
|
/// State for users feature
|
||||||
|
class UsersState extends Equatable {
|
||||||
|
final List<UserEntity> users;
|
||||||
|
final bool isLoading;
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
const UsersState({
|
||||||
|
this.users = const [],
|
||||||
|
this.isLoading = false,
|
||||||
|
this.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
UsersState copyWith({
|
||||||
|
List<UserEntity>? users,
|
||||||
|
bool? isLoading,
|
||||||
|
String? error,
|
||||||
|
}) {
|
||||||
|
return UsersState(
|
||||||
|
users: users ?? this.users,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
error: error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [users, isLoading, error];
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import '../../../../core/di/providers.dart';
|
import '../../../../core/di/providers.dart';
|
||||||
|
import '../../../../core/router/app_router.dart';
|
||||||
import '../widgets/warehouse_card.dart';
|
import '../widgets/warehouse_card.dart';
|
||||||
|
|
||||||
/// Warehouse selection page
|
/// Warehouse selection page
|
||||||
@@ -26,9 +26,11 @@ class _WarehouseSelectionPageState
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Load warehouses when page is first created
|
// Load warehouses and sync users when page is first created
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
ref.read(warehouseProvider.notifier).loadWarehouses();
|
ref.read(warehouseProvider.notifier).loadWarehouses();
|
||||||
|
// Sync users from API and save to local storage
|
||||||
|
ref.read(usersProvider.notifier).syncUsers();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,11 +172,14 @@ class _WarehouseSelectionPageState
|
|||||||
return WarehouseCard(
|
return WarehouseCard(
|
||||||
warehouse: warehouse,
|
warehouse: warehouse,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Select warehouse and navigate to operations
|
// Select warehouse and navigate directly to products page
|
||||||
ref.read(warehouseProvider.notifier).selectWarehouse(warehouse);
|
ref.read(warehouseProvider.notifier).selectWarehouse(warehouse);
|
||||||
|
|
||||||
// Navigate to operations page
|
// Navigate to products page with warehouse data
|
||||||
context.push('/operations', extra: warehouse);
|
context.pushToProducts(
|
||||||
|
warehouse: warehouse,
|
||||||
|
operationType: 'import', // Default to import
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
18
lib/hive_registrar.g.dart
Normal file
18
lib/hive_registrar.g.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Generated by Hive CE
|
||||||
|
// Do not modify
|
||||||
|
// Check in to version control
|
||||||
|
|
||||||
|
import 'package:hive_ce/hive.dart';
|
||||||
|
import 'package:minhthu/features/users/data/models/user_model.dart';
|
||||||
|
|
||||||
|
extension HiveRegistrar on HiveInterface {
|
||||||
|
void registerAdapters() {
|
||||||
|
registerAdapter(UserModelAdapter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||||
|
void registerAdapters() {
|
||||||
|
registerAdapter(UserModelAdapter());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
import 'core/router/app_router.dart';
|
import 'core/router/app_router.dart';
|
||||||
|
import 'features/users/data/models/user_model.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// Initialize Hive
|
||||||
|
final appDocumentDir = await getApplicationDocumentsDirectory();
|
||||||
|
await Hive.initFlutter(appDocumentDir.path);
|
||||||
|
|
||||||
|
// Register Hive adapters
|
||||||
|
Hive.registerAdapter(UserModelAdapter());
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
const ProviderScope(
|
const ProviderScope(
|
||||||
child: MyApp(),
|
child: MyApp(),
|
||||||
|
|||||||
@@ -633,7 +633,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ dependencies:
|
|||||||
dartz: ^0.10.1
|
dartz: ^0.10.1
|
||||||
get_it: ^7.6.4
|
get_it: ^7.6.4
|
||||||
flutter_secure_storage: ^9.0.0
|
flutter_secure_storage: ^9.0.0
|
||||||
|
path_provider: ^2.1.0
|
||||||
|
|
||||||
# Data Classes & Serialization
|
# Data Classes & Serialization
|
||||||
freezed_annotation: ^2.4.1
|
freezed_annotation: ^2.4.1
|
||||||
|
|||||||
Reference in New Issue
Block a user