# Flutter Warehouse Management App Guidelines ## App Overview Warehouse management app for importing and exporting products with authentication. ## App Flow 1. **Login Screen**: User authentication → Store access token 2. **Select Warehouse Screen**: Choose warehouse from list 3. **Operation Selection Screen**: Choose Import or Export products 4. **Product List Screen**: Display products based on operation type ## Project Structure ``` lib/ core/ constants/ api_endpoints.dart theme/ app_theme.dart widgets/ custom_button.dart loading_indicator.dart network/ api_client.dart api_response.dart storage/ secure_storage.dart features/ auth/ data/ datasources/ auth_remote_datasource.dart models/ login_request_model.dart login_response_model.dart repositories/ auth_repository_impl.dart domain/ entities/ user_entity.dart repositories/ auth_repository.dart usecases/ login_usecase.dart presentation/ providers/ auth_provider.dart pages/ login_page.dart widgets/ login_form.dart warehouse/ data/ datasources/ warehouse_remote_datasource.dart models/ warehouse_model.dart repositories/ warehouse_repository_impl.dart domain/ entities/ warehouse_entity.dart repositories/ warehouse_repository.dart usecases/ get_warehouses_usecase.dart presentation/ providers/ warehouse_provider.dart pages/ warehouse_selection_page.dart widgets/ warehouse_card.dart operation/ presentation/ pages/ operation_selection_page.dart widgets/ operation_card.dart products/ data/ datasources/ products_remote_datasource.dart models/ product_model.dart repositories/ products_repository_impl.dart domain/ entities/ product_entity.dart repositories/ products_repository.dart usecases/ get_products_usecase.dart presentation/ providers/ products_provider.dart pages/ products_page.dart widgets/ product_list_item.dart main.dart ``` ## API Response Format All API responses follow this structure: ```dart class ApiResponse { final T? value; final bool isSuccess; final bool isFailure; final List errors; final List errorCodes; ApiResponse({ this.value, required this.isSuccess, required this.isFailure, this.errors = const [], this.errorCodes = const [], }); factory ApiResponse.fromJson( Map json, T Function(dynamic)? fromJsonT, ) { return ApiResponse( value: json['Value'] != null && fromJsonT != null ? fromJsonT(json['Value']) : json['Value'], isSuccess: json['IsSuccess'] ?? false, isFailure: json['IsFailure'] ?? false, errors: List.from(json['Errors'] ?? []), errorCodes: List.from(json['ErrorCodes'] ?? []), ); } } ``` ## Data Models ### User Model ```dart class User { final String userId; final String username; final String accessToken; final String? refreshToken; User({ required this.userId, required this.username, required this.accessToken, this.refreshToken, }); factory User.fromJson(Map json) { return User( userId: json['userId'], username: json['username'], accessToken: json['accessToken'], refreshToken: json['refreshToken'], ); } } ``` ### Warehouse Model ```dart class Warehouse { final int id; final String name; final String code; final String? description; final bool isNGWareHouse; final int totalCount; Warehouse({ required this.id, required this.name, required this.code, this.description, required this.isNGWareHouse, required this.totalCount, }); factory Warehouse.fromJson(Map json) { return Warehouse( id: json['Id'] ?? 0, name: json['Name'] ?? '', code: json['Code'] ?? '', description: json['Description'], isNGWareHouse: json['IsNGWareHouse'] ?? false, totalCount: json['TotalCount'] ?? 0, ); } Map toJson() { return { 'Id': id, 'Name': name, 'Code': code, 'Description': description, 'IsNGWareHouse': isNGWareHouse, 'TotalCount': totalCount, }; } } ``` ### Product Model ```dart class Product { final int id; final String name; final String code; final String fullName; final String? description; final String? lotCode; final String? lotNumber; final String? logo; final String? barcode; // Quantity fields final int quantity; final int totalQuantity; final int passedQuantity; final double? passedQuantityWeight; final int issuedQuantity; final double? issuedQuantityWeight; final int piecesInStock; final double weightInStock; // Weight and pieces final double weight; final int pieces; final double conversionRate; final double? percent; // Price and status final double? price; final bool isActive; final bool isConfirm; final int? productStatusId; final int productTypeId; // Relations final int? orderId; final int? parentId; final int? receiverStageId; final dynamic order; // Dates final String? startDate; final String? endDate; // Lists final List productions; final List customerProducts; final List productStages; final dynamic childrenProducts; final dynamic productStageWareHouses; final dynamic productStageDetailWareHouses; final dynamic productExportExcelSheetDataModels; final dynamic materialLabels; final dynamic materials; final dynamic images; final dynamic attachmentFiles; Product({ required this.id, required this.name, required this.code, required this.fullName, this.description, this.lotCode, this.lotNumber, this.logo, this.barcode, required this.quantity, required this.totalQuantity, required this.passedQuantity, this.passedQuantityWeight, required this.issuedQuantity, this.issuedQuantityWeight, required this.piecesInStock, required this.weightInStock, required this.weight, required this.pieces, required this.conversionRate, this.percent, this.price, required this.isActive, required this.isConfirm, this.productStatusId, required this.productTypeId, this.orderId, this.parentId, this.receiverStageId, this.order, this.startDate, this.endDate, this.productions = const [], this.customerProducts = const [], this.productStages = const [], this.childrenProducts, this.productStageWareHouses, this.productStageDetailWareHouses, this.productExportExcelSheetDataModels, this.materialLabels, this.materials, this.images, this.attachmentFiles, }); factory Product.fromJson(Map json) { return Product( id: json['Id'] ?? 0, name: json['Name'] ?? '', code: json['Code'] ?? '', fullName: json['FullName'] ?? '', description: json['Description'], lotCode: json['LotCode'], lotNumber: json['LotNumber'], logo: json['Logo'], barcode: json['Barcode'], quantity: json['Quantity'] ?? 0, totalQuantity: json['TotalQuantity'] ?? 0, passedQuantity: json['PassedQuantity'] ?? 0, passedQuantityWeight: json['PassedQuantityWeight']?.toDouble(), issuedQuantity: json['IssuedQuantity'] ?? 0, issuedQuantityWeight: json['IssuedQuantityWeight']?.toDouble(), piecesInStock: json['PiecesInStock'] ?? 0, weightInStock: (json['WeightInStock'] ?? 0).toDouble(), weight: (json['Weight'] ?? 0).toDouble(), pieces: json['Pieces'] ?? 0, conversionRate: (json['ConversionRate'] ?? 0).toDouble(), percent: json['Percent']?.toDouble(), price: json['Price']?.toDouble(), isActive: json['IsActive'] ?? true, isConfirm: json['IsConfirm'] ?? false, productStatusId: json['ProductStatusId'], productTypeId: json['ProductTypeId'] ?? 0, orderId: json['OrderId'], parentId: json['ParentId'], receiverStageId: json['ReceiverStageId'], order: json['Order'], startDate: json['StartDate'], endDate: json['EndDate'], productions: json['Productions'] ?? [], customerProducts: json['CustomerProducts'] ?? [], productStages: json['ProductStages'] ?? [], childrenProducts: json['ChildrenProducts'], productStageWareHouses: json['ProductStageWareHouses'], productStageDetailWareHouses: json['ProductStageDetailWareHouses'], productExportExcelSheetDataModels: json['ProductExportExcelSheetDataModels'], materialLabels: json['MaterialLabels'], materials: json['Materials'], images: json['Images'], attachmentFiles: json['AttachmentFiles'], ); } Map toJson() { return { 'Id': id, 'Name': name, 'Code': code, 'FullName': fullName, 'Description': description, 'LotCode': lotCode, 'LotNumber': lotNumber, 'Logo': logo, 'Barcode': barcode, 'Quantity': quantity, 'TotalQuantity': totalQuantity, 'PassedQuantity': passedQuantity, 'PassedQuantityWeight': passedQuantityWeight, 'IssuedQuantity': issuedQuantity, 'IssuedQuantityWeight': issuedQuantityWeight, 'PiecesInStock': piecesInStock, 'WeightInStock': weightInStock, 'Weight': weight, 'Pieces': pieces, 'ConversionRate': conversionRate, 'Percent': percent, 'Price': price, 'IsActive': isActive, 'IsConfirm': isConfirm, 'ProductStatusId': productStatusId, 'ProductTypeId': productTypeId, 'OrderId': orderId, 'ParentId': parentId, 'ReceiverStageId': receiverStageId, 'Order': order, 'StartDate': startDate, 'EndDate': endDate, 'Productions': productions, 'CustomerProducts': customerProducts, 'ProductStages': productStages, 'ChildrenProducts': childrenProducts, 'ProductStageWareHouses': productStageWareHouses, 'ProductStageDetailWareHouses': productStageDetailWareHouses, 'ProductExportExcelSheetDataModels': productExportExcelSheetDataModels, 'MaterialLabels': materialLabels, 'Materials': materials, 'Images': images, 'AttachmentFiles': attachmentFiles, }; } } ``` ### Login Request Model ```dart class LoginRequest { final String username; final String password; LoginRequest({ required this.username, required this.password, }); Map toJson() => { 'username': username, 'password': password, }; } ``` ## Screen Layouts ### Login Screen ``` ┌─────────────────────────┐ │ │ │ [App Logo] │ │ │ │ Warehouse Manager │ │ │ ├─────────────────────────┤ │ │ │ Username: [__________] │ │ │ │ Password: [__________] │ │ │ │ [Login Button] │ │ │ └─────────────────────────┘ ``` ### Select Warehouse Screen ``` ┌──────────────────────────────────┐ │ Select Warehouse │ ├──────────────────────────────────┤ │ │ │ ┌──────────────────────────────┐ │ │ │ Kho nguyên vật liệu │ │ │ │ Code: 001 │ │ │ │ Items: 8 │ │ │ └──────────────────────────────┘ │ │ │ │ ┌──────────────────────────────┐ │ │ │ Kho bán thành phẩm công đoạn│ │ │ │ Code: 002 │ │ │ │ Items: 8 │ │ │ └──────────────────────────────┘ │ │ │ │ ┌──────────────────────────────┐ │ │ │ Kho thành phẩm │ │ │ │ Code: 003 │ │ │ │ Items: 8 │ │ │ └──────────────────────────────┘ │ │ │ │ ┌──────────────────────────────┐ │ │ │ Kho tiêu hao │ │ │ │ Code: 004 │ │ │ │ Items: 8 • Để chứa phụ tùng │ │ │ └──────────────────────────────┘ │ │ │ └──────────────────────────────────┘ ``` ### Operation Selection Screen ``` ┌─────────────────────────┐ │ Select Operation │ │ Warehouse: 001 │ ├─────────────────────────┤ │ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ │ │ Import Products│ │ │ │ │ │ │ └─────────────────┘ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ │ │ Export Products│ │ │ │ │ │ │ └─────────────────┘ │ │ │ │ │ └─────────────────────────┘ ``` ### Products List Screen ``` ┌───────────────────────────────────┐ │ Products (Import) │ │ Warehouse: Kho nguyên vật liệu │ ├───────────────────────────────────┤ │ │ │ • SCM435 | Thép 435 │ │ Code: SCM435 │ │ Weight: 120.00 | Pieces: 1320 │ │ In Stock: 0 pcs (0.00 kg) │ │ Conversion Rate: 11.00 │ ├───────────────────────────────────┤ │ • SCM440 | Thép 440 │ │ Code: SCM440 │ │ Weight: 85.50 | Pieces: 950 │ │ In Stock: 150 pcs (12.75 kg) │ │ Conversion Rate: 11.20 │ ├───────────────────────────────────┤ │ • SS304 | Thép không gỉ │ │ Code: SS304 │ │ Weight: 200.00 | Pieces: 2000 │ │ In Stock: 500 pcs (50.00 kg) │ │ Conversion Rate: 10.00 │ ├───────────────────────────────────┤ │ │ └───────────────────────────────────┘ ``` ## State Management (Riverpod) ### Auth State ```dart class AuthState { final User? user; final bool isAuthenticated; final bool isLoading; final String? error; AuthState({ this.user, this.isAuthenticated = false, this.isLoading = false, this.error, }); AuthState copyWith({ User? user, bool? isAuthenticated, bool? isLoading, String? error, }) { return AuthState( user: user ?? this.user, isAuthenticated: isAuthenticated ?? this.isAuthenticated, isLoading: isLoading ?? this.isLoading, error: error, ); } } ``` ### Warehouse State ```dart class WarehouseState { final List warehouses; final Warehouse? selectedWarehouse; final bool isLoading; final String? error; WarehouseState({ this.warehouses = const [], this.selectedWarehouse, this.isLoading = false, this.error, }); WarehouseState copyWith({ List? warehouses, Warehouse? selectedWarehouse, bool? isLoading, String? error, }) { return WarehouseState( warehouses: warehouses ?? this.warehouses, selectedWarehouse: selectedWarehouse ?? this.selectedWarehouse, isLoading: isLoading ?? this.isLoading, error: error, ); } } ``` ### Products State ```dart class ProductsState { final List products; final String operationType; // 'import' or 'export' final bool isLoading; final String? error; ProductsState({ this.products = const [], this.operationType = 'import', this.isLoading = false, this.error, }); ProductsState copyWith({ List? products, String? operationType, bool? isLoading, String? error, }) { return ProductsState( products: products ?? this.products, operationType: operationType ?? this.operationType, isLoading: isLoading ?? this.isLoading, error: error, ); } } ``` ## Secure Storage ### Token Management ```dart class SecureStorage { static const _storage = FlutterSecureStorage(); static const _accessTokenKey = 'access_token'; static const _refreshTokenKey = 'refresh_token'; Future saveAccessToken(String token) async { await _storage.write(key: _accessTokenKey, value: token); } Future getAccessToken() async { return await _storage.read(key: _accessTokenKey); } Future saveRefreshToken(String token) async { await _storage.write(key: _refreshTokenKey, value: token); } Future getRefreshToken() async { return await _storage.read(key: _refreshTokenKey); } Future clearAll() async { await _storage.deleteAll(); } } ``` ## API Integration ### Available APIs (CURL format) ```bash # Login curl -X POST https://api.example.com/auth/login \ -H "Content-Type: application/json" \ -d '{"username": "user", "password": "pass"}' # Get Warehouses curl -X GET https://api.example.com/warehouses \ -H "Authorization: Bearer {access_token}" # Get Products curl -X GET https://api.example.com/products?warehouseId={id}&type={import/export} \ -H "Authorization: Bearer {access_token}" ``` ### Auth Remote Data Source ```dart abstract class AuthRemoteDataSource { Future login(LoginRequest request); } class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { final ApiClient apiClient; AuthRemoteDataSourceImpl(this.apiClient); @override Future login(LoginRequest request) async { final response = await apiClient.post( '/auth/login', data: request.toJson(), ); final apiResponse = ApiResponse.fromJson( response.data, (json) => User.fromJson(json), ); if (apiResponse.isSuccess && apiResponse.value != null) { return apiResponse.value!; } else { throw ServerException( apiResponse.errors.isNotEmpty ? apiResponse.errors.first : 'Login failed' ); } } } ``` ### Warehouse Remote Data Source ```dart abstract class WarehouseRemoteDataSource { Future> getWarehouses(); } class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource { final ApiClient apiClient; WarehouseRemoteDataSourceImpl(this.apiClient); @override Future> getWarehouses() async { final response = await apiClient.get('/warehouses'); final apiResponse = ApiResponse.fromJson( response.data, (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(), ); if (apiResponse.isSuccess && apiResponse.value != null) { return apiResponse.value!; } else { throw ServerException( apiResponse.errors.isNotEmpty ? apiResponse.errors.first : 'Failed to get warehouses' ); } } } ``` ### Products Remote Data Source ```dart abstract class ProductsRemoteDataSource { Future> getProducts(int warehouseId, String type); } class ProductsRemoteDataSourceImpl implements ProductsRemoteDataSource { final ApiClient apiClient; ProductsRemoteDataSourceImpl(this.apiClient); @override Future> getProducts(int warehouseId, String type) async { final response = await apiClient.get( '/products', queryParameters: { 'warehouseId': warehouseId, 'type': type, }, ); final apiResponse = ApiResponse.fromJson( response.data, (json) => (json as List).map((e) => Product.fromJson(e)).toList(), ); if (apiResponse.isSuccess && apiResponse.value != null) { return apiResponse.value!; } else { throw ServerException( apiResponse.errors.isNotEmpty ? apiResponse.errors.first : 'Failed to get products' ); } } } ``` ## Use Cases ### Login Use Case ```dart class LoginUseCase { final AuthRepository repository; final SecureStorage secureStorage; LoginUseCase(this.repository, this.secureStorage); Future> call(LoginRequest request) async { final result = await repository.login(request); return result.fold( (failure) => Left(failure), (user) async { // Save tokens to secure storage await secureStorage.saveAccessToken(user.accessToken); if (user.refreshToken != null) { await secureStorage.saveRefreshToken(user.refreshToken!); } return Right(user); }, ); } } ``` ### Get Warehouses Use Case ```dart class GetWarehousesUseCase { final WarehouseRepository repository; GetWarehousesUseCase(this.repository); Future>> call() async { return await repository.getWarehouses(); } } ``` ### Get Products Use Case ```dart class GetProductsUseCase { final ProductsRepository repository; GetProductsUseCase(this.repository); Future>> call( int warehouseId, String type, ) async { return await repository.getProducts(warehouseId, type); } } ``` ## Repository Pattern ### Auth Repository ```dart abstract class AuthRepository { Future> login(LoginRequest request); } class AuthRepositoryImpl implements AuthRepository { final AuthRemoteDataSource remoteDataSource; AuthRepositoryImpl(this.remoteDataSource); @override Future> login(LoginRequest request) async { try { final user = await remoteDataSource.login(request); return Right(user); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } catch (e) { return Left(ServerFailure(e.toString())); } } } ``` ### Warehouse Repository ```dart abstract class WarehouseRepository { Future>> getWarehouses(); } class WarehouseRepositoryImpl implements WarehouseRepository { final WarehouseRemoteDataSource remoteDataSource; WarehouseRepositoryImpl(this.remoteDataSource); @override Future>> getWarehouses() async { try { final warehouses = await remoteDataSource.getWarehouses(); return Right(warehouses); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } catch (e) { return Left(ServerFailure(e.toString())); } } } ``` ### Products Repository ```dart abstract class ProductsRepository { Future>> getProducts( int warehouseId, String type, ); } class ProductsRepositoryImpl implements ProductsRepository { final ProductsRemoteDataSource remoteDataSource; ProductsRepositoryImpl(this.remoteDataSource); @override Future>> getProducts( int warehouseId, String type, ) async { try { final products = await remoteDataSource.getProducts(warehouseId, type); return Right(products); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } catch (e) { return Left(ServerFailure(e.toString())); } } } ``` ## Navigation Flow ### Router Configuration (go_router) ```dart final router = GoRouter( initialLocation: '/login', routes: [ GoRoute( path: '/login', name: 'login', builder: (context, state) => LoginPage(), ), GoRoute( path: '/warehouses', name: 'warehouses', builder: (context, state) => WarehouseSelectionPage(), ), GoRoute( path: '/operations', name: 'operations', builder: (context, state) { final warehouse = state.extra as Warehouse; return OperationSelectionPage(warehouse: warehouse); }, ), GoRoute( path: '/products', name: 'products', builder: (context, state) { final params = state.extra as Map; return ProductsPage( warehouse: params['warehouse'], operationType: params['type'], ); }, ), ], redirect: (context, state) async { final secureStorage = SecureStorage(); final token = await secureStorage.getAccessToken(); final isAuthenticated = token != null; final isLoggingIn = state.matchedLocation == '/login'; if (!isAuthenticated && !isLoggingIn) { return '/login'; } if (isAuthenticated && isLoggingIn) { return '/warehouses'; } return null; }, ); ``` ## API Client with Interceptor ### Dio Configuration ```dart class ApiClient { late final Dio _dio; final SecureStorage _secureStorage; ApiClient(this._secureStorage) { _dio = Dio( BaseOptions( baseUrl: 'https://api.example.com', connectTimeout: Duration(seconds: 30), receiveTimeout: Duration(seconds: 30), headers: { 'Content-Type': 'application/json', }, ), ); _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { // Add token to headers final token = await _secureStorage.getAccessToken(); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } return handler.next(options); }, onError: (error, handler) async { // Handle 401 unauthorized if (error.response?.statusCode == 401) { // Clear tokens and redirect to login await _secureStorage.clearAll(); // Navigate to login } return handler.next(error); }, ), ); } Future get( String path, { Map? queryParameters, }) async { return await _dio.get(path, queryParameters: queryParameters); } Future post( String path, { dynamic data, }) async { return await _dio.post(path, data: data); } } ``` ## Dependencies ```yaml dependencies: flutter_riverpod: ^2.4.9 go_router: ^12.1.3 dio: ^5.3.2 dartz: ^0.10.1 get_it: ^7.6.4 flutter_secure_storage: ^9.0.0 dev_dependencies: flutter_test: ^3.0.0 mockito: ^5.4.2 build_runner: ^2.4.7 ``` ## Error Handling ```dart abstract class Failure { final String message; const Failure(this.message); } class ServerFailure extends Failure { const ServerFailure(String message) : super(message); } class NetworkFailure extends Failure { const NetworkFailure(String message) : super(message); } class AuthenticationFailure extends Failure { const AuthenticationFailure(String message) : super(message); } class ServerException implements Exception { final String message; const ServerException(this.message); } ``` ## Key Points - Store access token in flutter_secure_storage after successful login - All API responses use "Value" key for data - API responses follow IsSuccess/IsFailure pattern - Add Authorization header with Bearer token to all authenticated requests - Handle 401 errors by clearing tokens and redirecting to login - Use clean architecture with use cases and repository pattern - Navigation flow: Login → Warehouses → Operations → Products - Only login, get warehouses, and get products APIs are available currently - Other features (import/export operations) will use placeholder/mock data until APIs are ready