1095 lines
29 KiB
Markdown
1095 lines
29 KiB
Markdown
# 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<T> {
|
|
final T? value;
|
|
final bool isSuccess;
|
|
final bool isFailure;
|
|
final List<String> errors;
|
|
final List<String> errorCodes;
|
|
|
|
ApiResponse({
|
|
this.value,
|
|
required this.isSuccess,
|
|
required this.isFailure,
|
|
this.errors = const [],
|
|
this.errorCodes = const [],
|
|
});
|
|
|
|
factory ApiResponse.fromJson(
|
|
Map<String, dynamic> 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<String>.from(json['Errors'] ?? []),
|
|
errorCodes: List<String>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<dynamic> productions;
|
|
final List<dynamic> customerProducts;
|
|
final List<dynamic> productStages;
|
|
final dynamic childrenProducts;
|
|
final dynamic productStageWareHouses;
|
|
final dynamic productStageDetailWareHouses;
|
|
final dynamic productExportExcelSheetDataModels;
|
|
final dynamic materialLabels;
|
|
final dynamic materials;
|
|
final dynamic images;
|
|
final dynamic attachmentFiles;
|
|
|
|
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<String, dynamic> 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<String, dynamic> toJson() {
|
|
return {
|
|
'Id': id,
|
|
'Name': name,
|
|
'Code': code,
|
|
'FullName': fullName,
|
|
'Description': description,
|
|
'LotCode': lotCode,
|
|
'LotNumber': lotNumber,
|
|
'Logo': logo,
|
|
'Barcode': barcode,
|
|
'Quantity': quantity,
|
|
'TotalQuantity': totalQuantity,
|
|
'PassedQuantity': passedQuantity,
|
|
'PassedQuantityWeight': passedQuantityWeight,
|
|
'IssuedQuantity': issuedQuantity,
|
|
'IssuedQuantityWeight': issuedQuantityWeight,
|
|
'PiecesInStock': piecesInStock,
|
|
'WeightInStock': weightInStock,
|
|
'Weight': weight,
|
|
'Pieces': pieces,
|
|
'ConversionRate': conversionRate,
|
|
'Percent': percent,
|
|
'Price': price,
|
|
'IsActive': isActive,
|
|
'IsConfirm': isConfirm,
|
|
'ProductStatusId': productStatusId,
|
|
'ProductTypeId': productTypeId,
|
|
'OrderId': orderId,
|
|
'ParentId': parentId,
|
|
'ReceiverStageId': receiverStageId,
|
|
'Order': order,
|
|
'StartDate': startDate,
|
|
'EndDate': endDate,
|
|
'Productions': productions,
|
|
'CustomerProducts': customerProducts,
|
|
'ProductStages': productStages,
|
|
'ChildrenProducts': childrenProducts,
|
|
'ProductStageWareHouses': productStageWareHouses,
|
|
'ProductStageDetailWareHouses': productStageDetailWareHouses,
|
|
'ProductExportExcelSheetDataModels': productExportExcelSheetDataModels,
|
|
'MaterialLabels': materialLabels,
|
|
'Materials': materials,
|
|
'Images': images,
|
|
'AttachmentFiles': attachmentFiles,
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### Login Request Model
|
|
```dart
|
|
class LoginRequest {
|
|
final String username;
|
|
final String password;
|
|
|
|
LoginRequest({
|
|
required this.username,
|
|
required this.password,
|
|
});
|
|
|
|
Map<String, dynamic> 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<Warehouse> warehouses;
|
|
final Warehouse? selectedWarehouse;
|
|
final bool isLoading;
|
|
final String? error;
|
|
|
|
WarehouseState({
|
|
this.warehouses = const [],
|
|
this.selectedWarehouse,
|
|
this.isLoading = false,
|
|
this.error,
|
|
});
|
|
|
|
WarehouseState copyWith({
|
|
List<Warehouse>? 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<Product> 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<Product>? 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<void> saveAccessToken(String token) async {
|
|
await _storage.write(key: _accessTokenKey, value: token);
|
|
}
|
|
|
|
Future<String?> getAccessToken() async {
|
|
return await _storage.read(key: _accessTokenKey);
|
|
}
|
|
|
|
Future<void> saveRefreshToken(String token) async {
|
|
await _storage.write(key: _refreshTokenKey, value: token);
|
|
}
|
|
|
|
Future<String?> getRefreshToken() async {
|
|
return await _storage.read(key: _refreshTokenKey);
|
|
}
|
|
|
|
Future<void> 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<User> login(LoginRequest request);
|
|
}
|
|
|
|
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
|
final ApiClient apiClient;
|
|
|
|
AuthRemoteDataSourceImpl(this.apiClient);
|
|
|
|
@override
|
|
Future<User> 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<List<Warehouse>> getWarehouses();
|
|
}
|
|
|
|
class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource {
|
|
final ApiClient apiClient;
|
|
|
|
WarehouseRemoteDataSourceImpl(this.apiClient);
|
|
|
|
@override
|
|
Future<List<Warehouse>> 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<List<Product>> getProducts(int warehouseId, String type);
|
|
}
|
|
|
|
class ProductsRemoteDataSourceImpl implements ProductsRemoteDataSource {
|
|
final ApiClient apiClient;
|
|
|
|
ProductsRemoteDataSourceImpl(this.apiClient);
|
|
|
|
@override
|
|
Future<List<Product>> 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<Either<Failure, User>> 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<Either<Failure, List<Warehouse>>> call() async {
|
|
return await repository.getWarehouses();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get Products Use Case
|
|
```dart
|
|
class GetProductsUseCase {
|
|
final ProductsRepository repository;
|
|
|
|
GetProductsUseCase(this.repository);
|
|
|
|
Future<Either<Failure, List<Product>>> call(
|
|
int warehouseId,
|
|
String type,
|
|
) async {
|
|
return await repository.getProducts(warehouseId, type);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Repository Pattern
|
|
|
|
### Auth Repository
|
|
```dart
|
|
abstract class AuthRepository {
|
|
Future<Either<Failure, User>> login(LoginRequest request);
|
|
}
|
|
|
|
class AuthRepositoryImpl implements AuthRepository {
|
|
final AuthRemoteDataSource remoteDataSource;
|
|
|
|
AuthRepositoryImpl(this.remoteDataSource);
|
|
|
|
@override
|
|
Future<Either<Failure, User>> 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<Either<Failure, List<Warehouse>>> getWarehouses();
|
|
}
|
|
|
|
class WarehouseRepositoryImpl implements WarehouseRepository {
|
|
final WarehouseRemoteDataSource remoteDataSource;
|
|
|
|
WarehouseRepositoryImpl(this.remoteDataSource);
|
|
|
|
@override
|
|
Future<Either<Failure, List<Warehouse>>> 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<Either<Failure, List<Product>>> getProducts(
|
|
int warehouseId,
|
|
String type,
|
|
);
|
|
}
|
|
|
|
class ProductsRepositoryImpl implements ProductsRepository {
|
|
final ProductsRemoteDataSource remoteDataSource;
|
|
|
|
ProductsRepositoryImpl(this.remoteDataSource);
|
|
|
|
@override
|
|
Future<Either<Failure, List<Product>>> 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<String, dynamic>;
|
|
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<Response> get(
|
|
String path, {
|
|
Map<String, dynamic>? queryParameters,
|
|
}) async {
|
|
return await _dio.get(path, queryParameters: queryParameters);
|
|
}
|
|
|
|
Future<Response> 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
|