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

View File

@@ -0,0 +1,649 @@
# Warehouse Feature
Complete implementation of the warehouse feature following **Clean Architecture** principles.
## Architecture Overview
This feature follows a three-layer clean architecture pattern:
```
Presentation Layer (UI)
↓ (uses)
Domain Layer (Business Logic)
↓ (uses)
Data Layer (API & Data Sources)
```
### Key Principles
- **Separation of Concerns**: Each layer has a single responsibility
- **Dependency Inversion**: Outer layers depend on inner layers, not vice versa
- **Testability**: Each layer can be tested independently
- **Maintainability**: Changes in one layer don't affect others
## Project Structure
```
lib/features/warehouse/
├── data/
│ ├── datasources/
│ │ └── warehouse_remote_datasource.dart # API calls using ApiClient
│ ├── models/
│ │ └── warehouse_model.dart # Data transfer objects with JSON serialization
│ └── repositories/
│ └── warehouse_repository_impl.dart # Repository implementation
├── domain/
│ ├── entities/
│ │ └── warehouse_entity.dart # Pure business models
│ ├── repositories/
│ │ └── warehouse_repository.dart # Repository interface/contract
│ └── usecases/
│ └── get_warehouses_usecase.dart # Business logic use cases
├── presentation/
│ ├── pages/
│ │ └── warehouse_selection_page.dart # Main warehouse selection screen
│ ├── providers/
│ │ └── warehouse_provider.dart # Riverpod state management
│ └── widgets/
│ └── warehouse_card.dart # Reusable warehouse card widget
├── warehouse_exports.dart # Barrel file for clean imports
├── warehouse_provider_setup_example.dart # Provider setup guide
└── README.md # This file
```
## Layer Details
### 1. Domain Layer (Core Business Logic)
The innermost layer that contains business entities, repository interfaces, and use cases. **No dependencies on external frameworks or packages** (except dartz for Either).
#### Entities
`domain/entities/warehouse_entity.dart`
- Pure Dart class representing a warehouse
- No JSON serialization logic
- Contains business rules and validations
- Extends Equatable for value comparison
```dart
class WarehouseEntity extends Equatable {
final int id;
final String name;
final String code;
final String? description;
final bool isNGWareHouse;
final int totalCount;
bool get hasItems => totalCount > 0;
bool get isNGType => isNGWareHouse;
}
```
#### Repository Interface
`domain/repositories/warehouse_repository.dart`
- Abstract interface defining data operations
- Returns `Either<Failure, T>` for error handling
- Implementation is provided by the data layer
```dart
abstract class WarehouseRepository {
Future<Either<Failure, List<WarehouseEntity>>> getWarehouses();
}
```
#### Use Cases
`domain/usecases/get_warehouses_usecase.dart`
- Single responsibility: fetch warehouses
- Encapsulates business logic
- Depends only on repository interface
```dart
class GetWarehousesUseCase {
final WarehouseRepository repository;
Future<Either<Failure, List<WarehouseEntity>>> call() async {
return await repository.getWarehouses();
}
}
```
### 2. Data Layer (External Data Management)
Handles all data operations including API calls, JSON serialization, and error handling.
#### Models
`data/models/warehouse_model.dart`
- Extends domain entity
- Adds JSON serialization (`fromJson`, `toJson`)
- Maps API response format to domain entities
- Matches API field naming (PascalCase)
```dart
class WarehouseModel extends WarehouseEntity {
factory WarehouseModel.fromJson(Map<String, dynamic> json) {
return WarehouseModel(
id: json['Id'] ?? 0,
name: json['Name'] ?? '',
code: json['Code'] ?? '',
description: json['Description'],
isNGWareHouse: json['IsNGWareHouse'] ?? false,
totalCount: json['TotalCount'] ?? 0,
);
}
}
```
#### Data Sources
`data/datasources/warehouse_remote_datasource.dart`
- Interface + implementation pattern
- Makes API calls using `ApiClient`
- Parses `ApiResponse` wrapper
- Throws typed exceptions (`ServerException`, `NetworkException`)
```dart
class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource {
Future<List<WarehouseModel>> getWarehouses() async {
final response = await apiClient.get('/warehouses');
final apiResponse = ApiResponse.fromJson(
response.data,
(json) => (json as List).map((e) => WarehouseModel.fromJson(e)).toList(),
);
if (apiResponse.isSuccess && apiResponse.value != null) {
return apiResponse.value!;
} else {
throw ServerException(apiResponse.errors.first);
}
}
}
```
#### Repository Implementation
`data/repositories/warehouse_repository_impl.dart`
- Implements domain repository interface
- Coordinates data sources
- Converts exceptions to failures
- Maps models to entities
```dart
class WarehouseRepositoryImpl implements WarehouseRepository {
@override
Future<Either<Failure, List<WarehouseEntity>>> getWarehouses() async {
try {
final warehouses = await remoteDataSource.getWarehouses();
final entities = warehouses.map((model) => model.toEntity()).toList();
return Right(entities);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
}
}
}
```
### 3. Presentation Layer (UI & State Management)
Handles UI rendering, user interactions, and state management using Riverpod.
#### State Management
`presentation/providers/warehouse_provider.dart`
- `WarehouseState`: Immutable state class
- `warehouses`: List of warehouses
- `selectedWarehouse`: Currently selected warehouse
- `isLoading`: Loading indicator
- `error`: Error message
- `WarehouseNotifier`: StateNotifier managing state
- `loadWarehouses()`: Fetch warehouses from API
- `selectWarehouse()`: Select a warehouse
- `refresh()`: Reload warehouses
- `clearError()`: Clear error state
```dart
class WarehouseState {
final List<WarehouseEntity> warehouses;
final WarehouseEntity? selectedWarehouse;
final bool isLoading;
final String? error;
}
class WarehouseNotifier extends StateNotifier<WarehouseState> {
Future<void> loadWarehouses() async {
state = state.setLoading();
final result = await getWarehousesUseCase();
result.fold(
(failure) => state = state.setError(failure.message),
(warehouses) => state = state.setSuccess(warehouses),
);
}
}
```
#### Pages
`presentation/pages/warehouse_selection_page.dart`
- ConsumerStatefulWidget using Riverpod
- Loads warehouses on initialization
- Displays different UI states:
- Loading: CircularProgressIndicator
- Error: Error message with retry button
- Empty: No warehouses message
- Success: List of warehouse cards
- Pull-to-refresh support
- Navigation to operations page on selection
#### Widgets
`presentation/widgets/warehouse_card.dart`
- Reusable warehouse card component
- Displays:
- Warehouse name (title)
- Code (with QR icon)
- Total items count (with inventory icon)
- Description (if available)
- NG warehouse badge (if applicable)
- Material Design 3 styling
- Tap to select functionality
## API Integration
### Endpoint
```
GET /warehouses
```
### Request
```bash
curl -X GET https://api.example.com/warehouses \
-H "Authorization: Bearer {access_token}"
```
### Response Format
```json
{
"Value": [
{
"Id": 1,
"Name": "Kho nguyên vật liệu",
"Code": "001",
"Description": "Kho chứa nguyên vật liệu",
"IsNGWareHouse": false,
"TotalCount": 8
},
{
"Id": 2,
"Name": "Kho bán thành phẩm công đoạn",
"Code": "002",
"Description": null,
"IsNGWareHouse": false,
"TotalCount": 12
}
],
"IsSuccess": true,
"IsFailure": false,
"Errors": [],
"ErrorCodes": []
}
```
## Setup & Integration
### 1. Install Dependencies
Ensure your `pubspec.yaml` includes:
```yaml
dependencies:
flutter_riverpod: ^2.4.9
dio: ^5.3.2
dartz: ^0.10.1
equatable: ^2.0.5
flutter_secure_storage: ^9.0.0
```
### 2. Set Up Providers
Create or update your provider configuration file (e.g., `lib/core/di/providers.dart`):
```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../features/warehouse/warehouse_exports.dart';
// Core providers (if not already set up)
final secureStorageProvider = Provider<SecureStorage>((ref) {
return SecureStorage();
});
final apiClientProvider = Provider<ApiClient>((ref) {
final secureStorage = ref.watch(secureStorageProvider);
return ApiClient(secureStorage);
});
// Warehouse data layer providers
final warehouseRemoteDataSourceProvider = Provider<WarehouseRemoteDataSource>((ref) {
final apiClient = ref.watch(apiClientProvider);
return WarehouseRemoteDataSourceImpl(apiClient);
});
final warehouseRepositoryProvider = Provider((ref) {
final remoteDataSource = ref.watch(warehouseRemoteDataSourceProvider);
return WarehouseRepositoryImpl(remoteDataSource);
});
// Warehouse domain layer providers
final getWarehousesUseCaseProvider = Provider((ref) {
final repository = ref.watch(warehouseRepositoryProvider);
return GetWarehousesUseCase(repository);
});
// Warehouse presentation layer providers
final warehouseProvider = StateNotifierProvider<WarehouseNotifier, WarehouseState>((ref) {
final getWarehousesUseCase = ref.watch(getWarehousesUseCaseProvider);
return WarehouseNotifier(getWarehousesUseCase);
});
```
### 3. Update WarehouseSelectionPage
Replace the TODO comments in `warehouse_selection_page.dart`:
```dart
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(warehouseProvider.notifier).loadWarehouses();
});
}
@override
Widget build(BuildContext context) {
final state = ref.watch(warehouseProvider);
// Rest of the implementation...
}
```
### 4. Add to Router
Using go_router:
```dart
GoRoute(
path: '/warehouses',
name: 'warehouses',
builder: (context, state) => const WarehouseSelectionPage(),
),
GoRoute(
path: '/operations',
name: 'operations',
builder: (context, state) {
final warehouse = state.extra as WarehouseEntity;
return OperationSelectionPage(warehouse: warehouse);
},
),
```
### 5. Navigate to Warehouse Page
```dart
// From login page after successful authentication
context.go('/warehouses');
// Or using Navigator
Navigator.of(context).pushNamed('/warehouses');
```
## Usage Examples
### Loading Warehouses
```dart
// In a widget
ElevatedButton(
onPressed: () {
ref.read(warehouseProvider.notifier).loadWarehouses();
},
child: const Text('Load Warehouses'),
)
```
### Watching State
```dart
// In a ConsumerWidget
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(warehouseProvider);
if (state.isLoading) {
return const CircularProgressIndicator();
}
if (state.error != null) {
return Text('Error: ${state.error}');
}
return ListView.builder(
itemCount: state.warehouses.length,
itemBuilder: (context, index) {
final warehouse = state.warehouses[index];
return ListTile(title: Text(warehouse.name));
},
);
}
```
### Selecting a Warehouse
```dart
// Select warehouse and navigate
void onWarehouseTap(WarehouseEntity warehouse) {
ref.read(warehouseProvider.notifier).selectWarehouse(warehouse);
context.push('/operations', extra: warehouse);
}
```
### Pull to Refresh
```dart
RefreshIndicator(
onRefresh: () => ref.read(warehouseProvider.notifier).refresh(),
child: ListView(...),
)
```
### Accessing Selected Warehouse
```dart
// In another page
final state = ref.watch(warehouseProvider);
final selectedWarehouse = state.selectedWarehouse;
if (selectedWarehouse != null) {
Text('Current: ${selectedWarehouse.name}');
}
```
## Error Handling
The feature uses dartz's `Either` type for functional error handling:
```dart
// In use case or repository
Future<Either<Failure, List<WarehouseEntity>>> getWarehouses() async {
try {
final warehouses = await remoteDataSource.getWarehouses();
return Right(warehouses); // Success
} on ServerException catch (e) {
return Left(ServerFailure(e.message)); // Failure
}
}
// In presentation layer
result.fold(
(failure) => print('Error: ${failure.message}'),
(warehouses) => print('Success: ${warehouses.length} items'),
);
```
### Failure Types
- `ServerFailure`: API errors, HTTP errors
- `NetworkFailure`: Connection issues, timeouts
- `CacheFailure`: Local storage errors (if implemented)
## Testing
### Unit Tests
**Test Use Case:**
```dart
test('should get warehouses from repository', () async {
// Arrange
when(mockRepository.getWarehouses())
.thenAnswer((_) async => Right(mockWarehouses));
// Act
final result = await useCase();
// Assert
expect(result, Right(mockWarehouses));
verify(mockRepository.getWarehouses());
});
```
**Test Repository:**
```dart
test('should return warehouses when remote call is successful', () async {
// Arrange
when(mockRemoteDataSource.getWarehouses())
.thenAnswer((_) async => mockWarehouseModels);
// Act
final result = await repository.getWarehouses();
// Assert
expect(result.isRight(), true);
});
```
**Test Notifier:**
```dart
test('should emit loading then success when warehouses are loaded', () async {
// Arrange
when(mockUseCase()).thenAnswer((_) async => Right(mockWarehouses));
// Act
await notifier.loadWarehouses();
// Assert
expect(notifier.state.isLoading, false);
expect(notifier.state.warehouses, mockWarehouses);
});
```
### Widget Tests
```dart
testWidgets('should display warehouse list when loaded', (tester) async {
// Arrange
final container = ProviderContainer(
overrides: [
warehouseProvider.overrideWith((ref) => MockWarehouseNotifier()),
],
);
// Act
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: const WarehouseSelectionPage(),
),
);
// Assert
expect(find.byType(WarehouseCard), findsWidgets);
});
```
## Best Practices
1. **Always use Either for error handling** - Don't throw exceptions across layers
2. **Keep domain layer pure** - No Flutter/external dependencies
3. **Use value objects** - Entities should be immutable
4. **Single responsibility** - Each class has one reason to change
5. **Dependency inversion** - Depend on abstractions, not concretions
6. **Test each layer independently** - Use mocks and test doubles
## Common Issues
### Provider Not Found
**Error:** `ProviderNotFoundException`
**Solution:** Make sure you've set up all providers in your provider configuration file and wrapped your app with `ProviderScope`.
### Null Safety Issues
**Error:** `Null check operator used on a null value`
**Solution:** Always check for null before accessing optional fields:
```dart
if (warehouse.description != null) {
Text(warehouse.description!);
}
```
### API Response Format Mismatch
**Error:** `ServerException: Invalid response format`
**Solution:** Verify that the API response matches the expected format in `ApiResponse.fromJson` and `WarehouseModel.fromJson`.
## Future Enhancements
- [ ] Add caching with Hive for offline support
- [ ] Implement warehouse search functionality
- [ ] Add warehouse filtering (by type, name, etc.)
- [ ] Add pagination for large warehouse lists
- [ ] Implement warehouse CRUD operations
- [ ] Add warehouse analytics and statistics
## Related Features
- **Authentication**: `/lib/features/auth/` - User login and token management
- **Operations**: `/lib/features/operation/` - Import/Export selection
- **Products**: `/lib/features/products/` - Product listing per warehouse
## References
- [Clean Architecture by Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [Flutter Riverpod Documentation](https://riverpod.dev/)
- [Dartz Package for Functional Programming](https://pub.dev/packages/dartz)
- [Material Design 3](https://m3.material.io/)
---
**Last Updated:** 2025-10-27
**Version:** 1.0.0
**Author:** Claude Code