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,398 @@
# Warehouse Feature - Architecture Diagram
## Clean Architecture Layers
```
┌─────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ (UI, State Management, User Interactions) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌──────────────────────────────────┐ │
│ │ WarehouseCard │ │ WarehouseSelectionPage │ │
│ │ - Shows warehouse │ │ - Displays warehouse list │ │
│ │ information │ │ - Handles user selection │ │
│ └─────────────────────┘ │ - Pull to refresh │ │
│ │ - Loading/Error/Empty states │ │
│ └──────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────┐ │
│ │ WarehouseNotifier │ │
│ │ (StateNotifier) │ │
│ │ - loadWarehouses() │ │
│ │ - selectWarehouse() │ │
│ │ - refresh() │ │
│ └──────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────┐ │
│ │ WarehouseState │ │
│ │ - warehouses: List │ │
│ │ - selectedWarehouse: Warehouse? │ │
│ │ - isLoading: bool │ │
│ │ - error: String? │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↓ uses
┌─────────────────────────────────────────────────────────────────┐
│ DOMAIN LAYER │
│ (Business Logic, Entities, Use Cases) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ GetWarehousesUseCase │ │
│ │ - Encapsulates business logic for fetching warehouses │ │
│ │ - Single responsibility │ │
│ │ - Returns Either<Failure, List<WarehouseEntity>> │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ uses │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ WarehouseRepository (Interface) │ │
│ │ + getWarehouses(): Either<Failure, List<Warehouse>> │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ WarehouseEntity │ │
│ │ - id: int │ │
│ │ - name: String │ │
│ │ - code: String │ │
│ │ - description: String? │ │
│ │ - isNGWareHouse: bool │ │
│ │ - totalCount: int │ │
│ │ + hasItems: bool │ │
│ │ + isNGType: bool │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↓ implements
┌─────────────────────────────────────────────────────────────────┐
│ DATA LAYER │
│ (API Calls, Data Sources, Models) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ WarehouseRepositoryImpl │ │
│ │ - Implements WarehouseRepository interface │ │
│ │ - Coordinates data sources │ │
│ │ - Converts exceptions to failures │ │
│ │ - Maps models to entities │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ uses │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ WarehouseRemoteDataSource (Interface) │ │
│ │ + getWarehouses(): Future<List<WarehouseModel>> │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ WarehouseRemoteDataSourceImpl │ │
│ │ - Makes API calls using ApiClient │ │
│ │ - Parses ApiResponse wrapper │ │
│ │ - Throws ServerException or NetworkException │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ uses │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ WarehouseModel │ │
│ │ - Extends WarehouseEntity │ │
│ │ - Adds JSON serialization (fromJson, toJson) │ │
│ │ - Maps API fields to entity fields │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ uses │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ApiClient (Core) │ │
│ │ - Dio HTTP client wrapper │ │
│ │ - Adds authentication headers │ │
│ │ - Handles 401 errors │ │
│ │ - Logging and error handling │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Data Flow
### 1. Loading Warehouses Flow
```
User Action (Pull to Refresh / Page Load)
WarehouseSelectionPage
↓ calls
ref.read(warehouseProvider.notifier).loadWarehouses()
WarehouseNotifier.loadWarehouses()
↓ sets state
state = state.setLoading() → UI shows loading indicator
↓ calls
GetWarehousesUseCase.call()
↓ calls
WarehouseRepository.getWarehouses()
↓ calls
WarehouseRemoteDataSource.getWarehouses()
↓ makes HTTP request
ApiClient.get('/warehouses')
↓ API Response
{
"Value": [...],
"IsSuccess": true,
"IsFailure": false,
"Errors": [],
"ErrorCodes": []
}
↓ parse
List<WarehouseModel> from JSON
↓ convert
List<WarehouseEntity>
↓ wrap
Right(warehouses) or Left(failure)
↓ update state
state = state.setSuccess(warehouses)
UI rebuilds with warehouse list
```
### 2. Error Handling Flow
```
API Error / Network Error
ApiClient throws DioException
_handleDioError() converts to custom exception
ServerException or NetworkException
WarehouseRemoteDataSource catches and rethrows
WarehouseRepositoryImpl catches exception
Converts to Failure:
- ServerException → ServerFailure
- NetworkException → NetworkFailure
Returns Left(failure)
GetWarehousesUseCase returns Left(failure)
WarehouseNotifier receives Left(failure)
state = state.setError(failure.message)
UI shows error state with retry button
```
### 3. Warehouse Selection Flow
```
User taps on WarehouseCard
onTap callback triggered
_onWarehouseSelected(warehouse)
ref.read(warehouseProvider.notifier).selectWarehouse(warehouse)
state = state.setSelectedWarehouse(warehouse)
Navigation: context.push('/operations', extra: warehouse)
OperationSelectionPage receives warehouse
```
## Dependency Graph
```
┌─────────────────────────────────────────────────┐
│ Riverpod Providers │
├─────────────────────────────────────────────────┤
│ │
│ secureStorageProvider │
│ ↓ │
│ apiClientProvider │
│ ↓ │
│ warehouseRemoteDataSourceProvider │
│ ↓ │
│ warehouseRepositoryProvider │
│ ↓ │
│ getWarehousesUseCaseProvider │
│ ↓ │
│ warehouseProvider (StateNotifierProvider) │
│ ↓ │
│ WarehouseSelectionPage watches this provider │
│ │
└─────────────────────────────────────────────────┘
```
## File Dependencies
```
warehouse_selection_page.dart
↓ imports
- warehouse_entity.dart
- warehouse_card.dart
- warehouse_provider.dart (via DI setup)
warehouse_card.dart
↓ imports
- warehouse_entity.dart
warehouse_provider.dart
↓ imports
- warehouse_entity.dart
- get_warehouses_usecase.dart
get_warehouses_usecase.dart
↓ imports
- warehouse_entity.dart
- warehouse_repository.dart (interface)
warehouse_repository_impl.dart
↓ imports
- warehouse_entity.dart
- warehouse_repository.dart (interface)
- warehouse_remote_datasource.dart
warehouse_remote_datasource.dart
↓ imports
- warehouse_model.dart
- api_client.dart
- api_response.dart
warehouse_model.dart
↓ imports
- warehouse_entity.dart
```
## State Transitions
```
┌──────────────┐
│ Initial │
│ isLoading: F │
│ error: null │
│ warehouses:[]│
└──────────────┘
loadWarehouses()
┌──────────────┐
│ Loading │
│ isLoading: T │────────────────┐
│ error: null │ │
│ warehouses:[]│ │
└──────────────┘ │
↓ │
Success Failure
↓ ↓
┌──────────────┐ ┌──────────────┐
│ Success │ │ Error │
│ isLoading: F │ │ isLoading: F │
│ error: null │ │ error: "..." │
│ warehouses:[…]│ │ warehouses:[]│
└──────────────┘ └──────────────┘
↓ ↓
Selection Retry
↓ ↓
┌──────────────┐ (back to Loading)
│ Selected │
│ selected: W │
└──────────────┘
```
## API Response Parsing
```
Raw API Response (JSON)
{
"Value": [
{
"Id": 1,
"Name": "Warehouse A",
"Code": "001",
...
}
],
"IsSuccess": true,
...
}
ApiResponse.fromJson() parses wrapper
ApiResponse<List<WarehouseModel>> {
value: [WarehouseModel, WarehouseModel, ...],
isSuccess: true,
isFailure: false,
errors: [],
errorCodes: []
}
Check isSuccess
if (isSuccess && value != null)
return value!
else
throw ServerException(errors.first)
List<WarehouseModel>
map((model) => model.toEntity())
List<WarehouseEntity>
```
## Separation of Concerns
### Domain Layer
- **No dependencies** on Flutter, Dio, or other frameworks
- Contains **pure business logic**
- Defines **contracts** (repository interfaces)
- **Independent** and **testable**
### Data Layer
- **Implements** domain contracts
- Handles **external dependencies** (API, database)
- **Converts** between models and entities
- **Transforms** exceptions to failures
### Presentation Layer
- **Depends** only on domain layer
- Handles **UI rendering** and **user interactions**
- Manages **local state** with Riverpod
- **Observes** changes and **reacts** to state updates
## Testing Strategy
```
Unit Tests
├── Domain Layer
│ ├── Test entities (equality, methods)
│ ├── Test use cases (mock repository)
│ └── Verify business logic
├── Data Layer
│ ├── Test models (JSON serialization)
│ ├── Test data sources (mock ApiClient)
│ └── Test repository (mock data source)
└── Presentation Layer
├── Test notifier (mock use case)
└── Test state transitions
Widget Tests
├── Test UI rendering
├── Test user interactions
└── Test state-based UI changes
Integration Tests
├── Test complete flow
└── Test with real dependencies
```
## Benefits of This Architecture
1. **Testability**: Each layer can be tested independently with mocks
2. **Maintainability**: Changes in one layer don't affect others
3. **Scalability**: Easy to add new features following the same pattern
4. **Reusability**: Domain entities and use cases can be reused
5. **Separation**: Clear boundaries between UI, business logic, and data
6. **Flexibility**: Easy to swap implementations (e.g., change API client)
---
**Last Updated:** 2025-10-27
**Version:** 1.0.0

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

View File

@@ -0,0 +1,76 @@
import '../../../../core/errors/exceptions.dart';
import '../../../../core/network/api_client.dart';
import '../../../../core/network/api_response.dart';
import '../models/warehouse_model.dart';
/// Abstract interface for warehouse remote data source
abstract class WarehouseRemoteDataSource {
/// Get all warehouses from the API
///
/// Returns [List<WarehouseModel>] on success
/// Throws [ServerException] on API errors
/// Throws [NetworkException] on network errors
Future<List<WarehouseModel>> getWarehouses();
}
/// Implementation of warehouse remote data source
/// Uses ApiClient to make HTTP requests to the backend
class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource {
final ApiClient apiClient;
WarehouseRemoteDataSourceImpl(this.apiClient);
@override
Future<List<WarehouseModel>> getWarehouses() async {
try {
// Make POST request to /portalWareHouse/search endpoint
final response = await apiClient.post(
'/portalWareHouse/search',
data: {
'pageIndex': 0,
'pageSize': 100,
'Name': null,
'Code': null,
'sortExpression': null,
'sortDirection': null,
},
);
// Parse the API response wrapper
final apiResponse = ApiResponse.fromJson(
response.data,
(json) {
// Handle the list of warehouses
if (json is List) {
return json.map((e) => WarehouseModel.fromJson(e)).toList();
}
throw const ServerException('Invalid response format: expected List');
},
);
// Check if API call was successful
if (apiResponse.isSuccess && apiResponse.value != null) {
return apiResponse.value!;
} else {
// Extract error message from API response
final errorMessage = apiResponse.errors.isNotEmpty
? apiResponse.errors.first
: 'Failed to get warehouses';
throw ServerException(
errorMessage,
code: apiResponse.firstErrorCode,
);
}
} on ServerException {
rethrow;
} on NetworkException {
rethrow;
} catch (e) {
// Wrap any unexpected errors
throw ServerException(
'Unexpected error while fetching warehouses: ${e.toString()}',
);
}
}
}

View File

@@ -0,0 +1,100 @@
import '../../domain/entities/warehouse_entity.dart';
/// Warehouse data model
/// Extends domain entity and adds JSON serialization
/// Matches the API response format from backend
class WarehouseModel extends WarehouseEntity {
const WarehouseModel({
required super.id,
required super.name,
required super.code,
super.description,
required super.isNGWareHouse,
required super.totalCount,
});
/// Create a WarehouseModel from JSON
///
/// JSON format from API:
/// ```json
/// {
/// "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
/// }
/// ```
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,
);
}
/// Convert model to JSON
Map<String, dynamic> toJson() {
return {
'Id': id,
'Name': name,
'Code': code,
'Description': description,
'IsNGWareHouse': isNGWareHouse,
'TotalCount': totalCount,
};
}
/// Create from domain entity
factory WarehouseModel.fromEntity(WarehouseEntity entity) {
return WarehouseModel(
id: entity.id,
name: entity.name,
code: entity.code,
description: entity.description,
isNGWareHouse: entity.isNGWareHouse,
totalCount: entity.totalCount,
);
}
/// Convert to domain entity
WarehouseEntity toEntity() {
return WarehouseEntity(
id: id,
name: name,
code: code,
description: description,
isNGWareHouse: isNGWareHouse,
totalCount: totalCount,
);
}
/// Create a copy with modified fields
@override
WarehouseModel copyWith({
int? id,
String? name,
String? code,
String? description,
bool? isNGWareHouse,
int? totalCount,
}) {
return WarehouseModel(
id: id ?? this.id,
name: name ?? this.name,
code: code ?? this.code,
description: description ?? this.description,
isNGWareHouse: isNGWareHouse ?? this.isNGWareHouse,
totalCount: totalCount ?? this.totalCount,
);
}
@override
String toString() {
return 'WarehouseModel(id: $id, name: $name, code: $code, totalCount: $totalCount)';
}
}

View File

@@ -0,0 +1,39 @@
import 'package:dartz/dartz.dart';
import '../../../../core/errors/exceptions.dart';
import '../../../../core/errors/failures.dart';
import '../../domain/entities/warehouse_entity.dart';
import '../../domain/repositories/warehouse_repository.dart';
import '../datasources/warehouse_remote_datasource.dart';
/// Implementation of WarehouseRepository
/// Coordinates between data sources and domain layer
/// Converts exceptions to failures for proper error handling
class WarehouseRepositoryImpl implements WarehouseRepository {
final WarehouseRemoteDataSource remoteDataSource;
WarehouseRepositoryImpl(this.remoteDataSource);
@override
Future<Either<Failure, List<WarehouseEntity>>> getWarehouses() async {
try {
// Fetch warehouses from remote data source
final warehouses = await remoteDataSource.getWarehouses();
// Convert models to entities
final entities = warehouses
.map((model) => model.toEntity())
.toList();
return Right(entities);
} on ServerException catch (e) {
// Convert server exceptions to server failures
return Left(ServerFailure(e.message));
} on NetworkException catch (e) {
// Convert network exceptions to network failures
return Left(NetworkFailure(e.message));
} catch (e) {
// Handle any unexpected errors
return Left(ServerFailure('Unexpected error: ${e.toString()}'));
}
}
}

View File

@@ -0,0 +1,61 @@
import 'package:equatable/equatable.dart';
/// Warehouse domain entity
/// Pure business model with no dependencies on data layer
class WarehouseEntity extends Equatable {
final int id;
final String name;
final String code;
final String? description;
final bool isNGWareHouse;
final int totalCount;
const WarehouseEntity({
required this.id,
required this.name,
required this.code,
this.description,
required this.isNGWareHouse,
required this.totalCount,
});
@override
List<Object?> get props => [
id,
name,
code,
description,
isNGWareHouse,
totalCount,
];
@override
String toString() {
return 'WarehouseEntity(id: $id, name: $name, code: $code, totalCount: $totalCount)';
}
/// Check if warehouse has items
bool get hasItems => totalCount > 0;
/// Check if this is an NG (Not Good/Defect) warehouse
bool get isNGType => isNGWareHouse;
/// Create a copy with modified fields
WarehouseEntity copyWith({
int? id,
String? name,
String? code,
String? description,
bool? isNGWareHouse,
int? totalCount,
}) {
return WarehouseEntity(
id: id ?? this.id,
name: name ?? this.name,
code: code ?? this.code,
description: description ?? this.description,
isNGWareHouse: isNGWareHouse ?? this.isNGWareHouse,
totalCount: totalCount ?? this.totalCount,
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/warehouse_entity.dart';
/// Abstract repository interface for warehouse operations
/// Defines the contract for warehouse data operations
/// Implementations should handle data sources and convert exceptions to failures
abstract class WarehouseRepository {
/// Get all warehouses from the remote data source
///
/// Returns [Either<Failure, List<WarehouseEntity>>]
/// - Right: List of warehouses on success
/// - Left: Failure object with error details
Future<Either<Failure, List<WarehouseEntity>>> getWarehouses();
}

View File

@@ -0,0 +1,32 @@
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/warehouse_entity.dart';
import '../repositories/warehouse_repository.dart';
/// Use case for getting all warehouses
/// Encapsulates the business logic for fetching warehouses
///
/// Usage:
/// ```dart
/// final useCase = GetWarehousesUseCase(repository);
/// final result = await useCase();
///
/// result.fold(
/// (failure) => print('Error: ${failure.message}'),
/// (warehouses) => print('Loaded ${warehouses.length} warehouses'),
/// );
/// ```
class GetWarehousesUseCase {
final WarehouseRepository repository;
GetWarehousesUseCase(this.repository);
/// Execute the use case
///
/// Returns [Either<Failure, List<WarehouseEntity>>]
/// - Right: List of warehouses on success
/// - Left: Failure object with error details
Future<Either<Failure, List<WarehouseEntity>>> call() async {
return await repository.getWarehouses();
}
}

View File

@@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/di/providers.dart';
import '../widgets/warehouse_card.dart';
/// Warehouse selection page
/// Displays a list of warehouses and allows user to select one
///
/// Features:
/// - Loads warehouses on init
/// - Pull to refresh
/// - Loading, error, empty, and success states
/// - Navigate to operations page on warehouse selection
class WarehouseSelectionPage extends ConsumerStatefulWidget {
const WarehouseSelectionPage({super.key});
@override
ConsumerState<WarehouseSelectionPage> createState() =>
_WarehouseSelectionPageState();
}
class _WarehouseSelectionPageState
extends ConsumerState<WarehouseSelectionPage> {
@override
void initState() {
super.initState();
// Load warehouses when page is first created
Future.microtask(() {
ref.read(warehouseProvider.notifier).loadWarehouses();
});
}
@override
Widget build(BuildContext context) {
// Watch warehouse state
final warehouseState = ref.watch(warehouseProvider);
final warehouses = warehouseState.warehouses;
final isLoading = warehouseState.isLoading;
final error = warehouseState.error;
return Scaffold(
appBar: AppBar(
title: const Text('Select Warehouse'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
ref.read(warehouseProvider.notifier).loadWarehouses();
},
tooltip: 'Refresh',
),
],
),
body: _buildBody(
isLoading: isLoading,
error: error,
warehouses: warehouses,
),
);
}
/// Build body based on state
Widget _buildBody({
required bool isLoading,
required String? error,
required List warehouses,
}) {
// Loading state
if (isLoading && warehouses.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading warehouses...'),
],
),
);
}
// Error state
if (error != null && warehouses.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(
'Error Loading Warehouses',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
error,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: () {
ref.read(warehouseProvider.notifier).loadWarehouses();
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
),
],
),
),
);
}
// Empty state
if (warehouses.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'No Warehouses Available',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'There are no warehouses to display.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
OutlinedButton.icon(
onPressed: () {
ref.read(warehouseProvider.notifier).loadWarehouses();
},
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
),
],
),
),
);
}
// Success state - show warehouse list
return RefreshIndicator(
onRefresh: () async {
await ref.read(warehouseProvider.notifier).loadWarehouses();
},
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: warehouses.length,
itemBuilder: (context, index) {
final warehouse = warehouses[index];
return WarehouseCard(
warehouse: warehouse,
onTap: () {
// Select warehouse and navigate to operations
ref.read(warehouseProvider.notifier).selectWarehouse(warehouse);
// Navigate to operations page
context.go('/operations', extra: warehouse);
},
);
},
),
);
}
}

View File

@@ -0,0 +1,396 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/warehouse_entity.dart';
import '../widgets/warehouse_card.dart';
import '../../../../core/router/app_router.dart';
/// EXAMPLE: Warehouse selection page with proper navigation integration
///
/// This is a complete example showing how to integrate the warehouse selection
/// page with the new router. Use this as a reference when implementing the
/// actual warehouse provider and state management.
///
/// Key Features:
/// - Uses type-safe navigation with extension methods
/// - Proper error handling
/// - Loading states
/// - Pull to refresh
/// - Integration with router
class WarehouseSelectionPageExample extends ConsumerStatefulWidget {
const WarehouseSelectionPageExample({super.key});
@override
ConsumerState<WarehouseSelectionPageExample> createState() =>
_WarehouseSelectionPageExampleState();
}
class _WarehouseSelectionPageExampleState
extends ConsumerState<WarehouseSelectionPageExample> {
@override
void initState() {
super.initState();
// Load warehouses when page is first created
WidgetsBinding.instance.addPostFrameCallback((_) {
// TODO: Replace with actual provider
// ref.read(warehouseProvider.notifier).loadWarehouses();
});
}
@override
Widget build(BuildContext context) {
// TODO: Replace with actual provider
// final state = ref.watch(warehouseProvider);
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('Select Warehouse'),
backgroundColor: colorScheme.primaryContainer,
foregroundColor: colorScheme.onPrimaryContainer,
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => _handleLogout(context),
tooltip: 'Logout',
),
],
),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context) {
// For demonstration, showing example warehouse list
// In actual implementation, use state from provider:
// if (state.isLoading) return _buildLoadingState();
// if (state.error != null) return _buildErrorState(context, state.error!);
// if (!state.hasWarehouses) return _buildEmptyState(context);
// return _buildWarehouseList(state.warehouses);
// Example warehouses for demonstration
final exampleWarehouses = _getExampleWarehouses();
return _buildWarehouseList(exampleWarehouses);
}
/// Build loading state UI
Widget _buildLoadingState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'Loading warehouses...',
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
);
}
/// Build error state UI
Widget _buildErrorState(BuildContext context, String error) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: colorScheme.error,
),
const SizedBox(height: 16),
Text(
'Error Loading Warehouses',
style: theme.textTheme.titleLarge?.copyWith(
color: colorScheme.error,
),
),
const SizedBox(height: 8),
Text(
error,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: () {
// TODO: Replace with actual provider
// ref.read(warehouseProvider.notifier).loadWarehouses();
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
),
],
),
),
);
}
/// Build empty state UI
Widget _buildEmptyState(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'No Warehouses Available',
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'There are no warehouses to display.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 24),
OutlinedButton.icon(
onPressed: () {
// TODO: Replace with actual provider
// ref.read(warehouseProvider.notifier).loadWarehouses();
},
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
),
],
),
),
);
}
/// Build warehouse list UI
Widget _buildWarehouseList(List<WarehouseEntity> warehouses) {
return RefreshIndicator(
onRefresh: () async {
// TODO: Replace with actual provider
// await ref.read(warehouseProvider.notifier).refresh();
},
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: warehouses.length,
itemBuilder: (context, index) {
final warehouse = warehouses[index];
return WarehouseCard(
warehouse: warehouse,
onTap: () => _onWarehouseSelected(context, warehouse),
);
},
),
);
}
/// Handle warehouse selection
///
/// This is the key integration point with the router!
/// Uses the type-safe extension method to navigate to operations page
void _onWarehouseSelected(BuildContext context, WarehouseEntity warehouse) {
// TODO: Optionally save selected warehouse to provider
// ref.read(warehouseProvider.notifier).selectWarehouse(warehouse);
// Navigate to operations page using type-safe extension method
context.goToOperations(warehouse);
}
/// Handle logout
void _handleLogout(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Logout'),
content: const Text('Are you sure you want to logout?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Logout'),
),
],
),
);
if (confirmed == true && context.mounted) {
// TODO: Call logout from auth provider
// await ref.read(authProvider.notifier).logout();
// Router will automatically redirect to login page
// due to authentication state change
}
}
/// Get example warehouses for demonstration
List<WarehouseEntity> _getExampleWarehouses() {
return [
WarehouseEntity(
id: 1,
name: 'Kho nguyên vật liệu',
code: '001',
description: 'Warehouse for raw materials',
isNGWareHouse: false,
totalCount: 8,
),
WarehouseEntity(
id: 2,
name: 'Kho bán thành phẩm công đoạn',
code: '002',
description: 'Semi-finished goods warehouse',
isNGWareHouse: false,
totalCount: 8,
),
WarehouseEntity(
id: 3,
name: 'Kho thành phẩm',
code: '003',
description: 'Finished goods warehouse',
isNGWareHouse: false,
totalCount: 8,
),
WarehouseEntity(
id: 4,
name: 'Kho tiêu hao',
code: '004',
description: 'Để chứa phụ tùng',
isNGWareHouse: false,
totalCount: 8,
),
WarehouseEntity(
id: 5,
name: 'Kho NG',
code: '005',
description: 'Non-conforming products warehouse',
isNGWareHouse: true,
totalCount: 3,
),
];
}
}
/// EXAMPLE: Alternative approach using named routes
///
/// This shows how to use named routes instead of path-based navigation
class WarehouseSelectionWithNamedRoutesExample extends ConsumerWidget {
const WarehouseSelectionWithNamedRoutesExample({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Example warehouse for demonstration
final warehouse = WarehouseEntity(
id: 1,
name: 'Example Warehouse',
code: '001',
isNGWareHouse: false,
totalCount: 10,
);
return Scaffold(
appBar: AppBar(title: const Text('Named Routes Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
// Using named route navigation
context.goToOperationsNamed(warehouse);
},
child: const Text('Go to Operations (Named)'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// Using path-based navigation
context.goToOperations(warehouse);
},
child: const Text('Go to Operations (Path)'),
),
],
),
),
);
}
}
/// EXAMPLE: Navigation from operation to products
///
/// Shows how to navigate from operation selection to products page
class OperationNavigationExample extends StatelessWidget {
final WarehouseEntity warehouse;
const OperationNavigationExample({
super.key,
required this.warehouse,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Operation Navigation Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
// Navigate to products with import operation
context.goToProducts(
warehouse: warehouse,
operationType: 'import',
);
},
icon: const Icon(Icons.arrow_downward),
label: const Text('Import Products'),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
// Navigate to products with export operation
context.goToProducts(
warehouse: warehouse,
operationType: 'export',
);
},
icon: const Icon(Icons.arrow_upward),
label: const Text('Export Products'),
),
const SizedBox(height: 32),
OutlinedButton(
onPressed: () {
// Navigate back
context.goBack();
},
child: const Text('Go Back'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,146 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/warehouse_entity.dart';
import '../../domain/usecases/get_warehouses_usecase.dart';
/// Warehouse state that holds the current state of warehouse feature
class WarehouseState {
final List<WarehouseEntity> warehouses;
final WarehouseEntity? selectedWarehouse;
final bool isLoading;
final String? error;
const WarehouseState({
this.warehouses = const [],
this.selectedWarehouse,
this.isLoading = false,
this.error,
});
/// Create initial state
factory WarehouseState.initial() {
return const WarehouseState();
}
/// Create loading state
WarehouseState setLoading() {
return WarehouseState(
warehouses: warehouses,
selectedWarehouse: selectedWarehouse,
isLoading: true,
error: null,
);
}
/// Create success state
WarehouseState setSuccess(List<WarehouseEntity> warehouses) {
return WarehouseState(
warehouses: warehouses,
selectedWarehouse: selectedWarehouse,
isLoading: false,
error: null,
);
}
/// Create error state
WarehouseState setError(String error) {
return WarehouseState(
warehouses: warehouses,
selectedWarehouse: selectedWarehouse,
isLoading: false,
error: error,
);
}
/// Create state with selected warehouse
WarehouseState setSelectedWarehouse(WarehouseEntity? warehouse) {
return WarehouseState(
warehouses: warehouses,
selectedWarehouse: warehouse,
isLoading: isLoading,
error: error,
);
}
/// Create a copy with modified fields
WarehouseState copyWith({
List<WarehouseEntity>? warehouses,
WarehouseEntity? selectedWarehouse,
bool? isLoading,
String? error,
}) {
return WarehouseState(
warehouses: warehouses ?? this.warehouses,
selectedWarehouse: selectedWarehouse ?? this.selectedWarehouse,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
/// Check if warehouses are loaded
bool get hasWarehouses => warehouses.isNotEmpty;
/// Check if a warehouse is selected
bool get hasSelection => selectedWarehouse != null;
@override
String toString() {
return 'WarehouseState(warehouses: ${warehouses.length}, '
'selectedWarehouse: ${selectedWarehouse?.name}, '
'isLoading: $isLoading, error: $error)';
}
}
/// State notifier for warehouse operations
/// Manages the warehouse state and handles business logic
class WarehouseNotifier extends StateNotifier<WarehouseState> {
final GetWarehousesUseCase getWarehousesUseCase;
WarehouseNotifier(this.getWarehousesUseCase)
: super(WarehouseState.initial());
/// Load all warehouses from the API
Future<void> loadWarehouses() async {
// Set loading state
state = state.setLoading();
// Execute the use case
final result = await getWarehousesUseCase();
// Handle the result
result.fold(
(failure) {
// Set error state on failure
state = state.setError(failure.message);
},
(warehouses) {
// Set success state with warehouses
state = state.setSuccess(warehouses);
},
);
}
/// Select a warehouse
void selectWarehouse(WarehouseEntity warehouse) {
state = state.setSelectedWarehouse(warehouse);
}
/// Clear selected warehouse
void clearSelection() {
state = state.setSelectedWarehouse(null);
}
/// Refresh warehouses (reload from API)
Future<void> refresh() async {
await loadWarehouses();
}
/// Clear error message
void clearError() {
state = state.copyWith(error: null);
}
/// Reset state to initial
void reset() {
state = WarehouseState.initial();
}
}

View File

@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import '../../domain/entities/warehouse_entity.dart';
/// Reusable warehouse card widget
/// Displays warehouse information in a card format
class WarehouseCard extends StatelessWidget {
final WarehouseEntity warehouse;
final VoidCallback onTap;
const WarehouseCard({
super.key,
required this.warehouse,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Card(
elevation: 2,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Warehouse name
Text(
warehouse.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
// Warehouse code
Row(
children: [
Icon(
Icons.qr_code,
size: 16,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
'Code: ${warehouse.code}',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
const SizedBox(height: 4),
// Items count
Row(
children: [
Icon(
Icons.inventory_2_outlined,
size: 16,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
'Items: ${warehouse.totalCount}',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
// Description (if available)
if (warehouse.description != null &&
warehouse.description!.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
warehouse.description!,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
// NG Warehouse badge (if applicable)
if (warehouse.isNGWareHouse) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: colorScheme.errorContainer,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'NG Warehouse',
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.onErrorContainer,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
),
),
);
}
}

View File

@@ -0,0 +1,14 @@
// Domain Layer Exports
export 'domain/entities/warehouse_entity.dart';
export 'domain/repositories/warehouse_repository.dart';
export 'domain/usecases/get_warehouses_usecase.dart';
// Data Layer Exports
export 'data/models/warehouse_model.dart';
export 'data/datasources/warehouse_remote_datasource.dart';
export 'data/repositories/warehouse_repository_impl.dart';
// Presentation Layer Exports
export 'presentation/providers/warehouse_provider.dart';
export 'presentation/pages/warehouse_selection_page.dart';
export 'presentation/widgets/warehouse_card.dart';

View File

@@ -0,0 +1,153 @@
/// EXAMPLE FILE: How to set up the warehouse provider
///
/// This file demonstrates how to wire up all the warehouse feature dependencies
/// using Riverpod providers. Copy this setup to your actual provider configuration file.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/network/api_client.dart';
import '../../core/storage/secure_storage.dart';
import 'data/datasources/warehouse_remote_datasource.dart';
import 'data/repositories/warehouse_repository_impl.dart';
import 'domain/usecases/get_warehouses_usecase.dart';
import 'presentation/providers/warehouse_provider.dart';
// ==============================================================================
// STEP 1: Provide core dependencies (ApiClient, SecureStorage)
// ==============================================================================
// These should already be set up in your app. If not, add them to your
// core providers file (e.g., lib/core/di/providers.dart or lib/core/providers.dart)
// Provider for SecureStorage
final secureStorageProvider = Provider<SecureStorage>((ref) {
return SecureStorage();
});
// Provider for ApiClient
final apiClientProvider = Provider<ApiClient>((ref) {
final secureStorage = ref.watch(secureStorageProvider);
return ApiClient(secureStorage);
});
// ==============================================================================
// STEP 2: Data Layer Providers
// ==============================================================================
// Provider for WarehouseRemoteDataSource
final warehouseRemoteDataSourceProvider = Provider<WarehouseRemoteDataSource>((ref) {
final apiClient = ref.watch(apiClientProvider);
return WarehouseRemoteDataSourceImpl(apiClient);
});
// Provider for WarehouseRepository
final warehouseRepositoryProvider = Provider((ref) {
final remoteDataSource = ref.watch(warehouseRemoteDataSourceProvider);
return WarehouseRepositoryImpl(remoteDataSource);
});
// ==============================================================================
// STEP 3: Domain Layer Providers (Use Cases)
// ==============================================================================
// Provider for GetWarehousesUseCase
final getWarehousesUseCaseProvider = Provider((ref) {
final repository = ref.watch(warehouseRepositoryProvider);
return GetWarehousesUseCase(repository);
});
// ==============================================================================
// STEP 4: Presentation Layer Providers (State Management)
// ==============================================================================
// Provider for WarehouseNotifier (StateNotifier)
final warehouseProvider = StateNotifierProvider<WarehouseNotifier, WarehouseState>((ref) {
final getWarehousesUseCase = ref.watch(getWarehousesUseCaseProvider);
return WarehouseNotifier(getWarehousesUseCase);
});
// ==============================================================================
// USAGE IN WIDGETS
// ==============================================================================
/*
// Example 1: In WarehouseSelectionPage
class WarehouseSelectionPage extends ConsumerStatefulWidget {
const WarehouseSelectionPage({super.key});
@override
ConsumerState<WarehouseSelectionPage> createState() =>
_WarehouseSelectionPageState();
}
class _WarehouseSelectionPageState extends ConsumerState<WarehouseSelectionPage> {
@override
void initState() {
super.initState();
// Load warehouses when page is created
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(warehouseProvider.notifier).loadWarehouses();
});
}
@override
Widget build(BuildContext context) {
final state = ref.watch(warehouseProvider);
return Scaffold(
appBar: AppBar(title: const Text('Select Warehouse')),
body: state.isLoading
? const Center(child: CircularProgressIndicator())
: state.error != null
? Center(child: Text('Error: ${state.error}'))
: ListView.builder(
itemCount: state.warehouses.length,
itemBuilder: (context, index) {
final warehouse = state.warehouses[index];
return ListTile(
title: Text(warehouse.name),
subtitle: Text('Code: ${warehouse.code}'),
onTap: () {
ref.read(warehouseProvider.notifier).selectWarehouse(warehouse);
// Navigate to next screen
context.push('/operations', extra: warehouse);
},
);
},
),
);
}
}
// Example 2: Accessing selected warehouse in another page
class OperationSelectionPage extends ConsumerWidget {
const OperationSelectionPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(warehouseProvider);
final selectedWarehouse = state.selectedWarehouse;
return Scaffold(
appBar: AppBar(
title: Text('Warehouse: ${selectedWarehouse?.code ?? 'None'}'),
),
body: Center(
child: Text('Selected: ${selectedWarehouse?.name ?? 'No selection'}'),
),
);
}
}
// Example 3: Manually loading warehouses on button press
ElevatedButton(
onPressed: () {
ref.read(warehouseProvider.notifier).loadWarehouses();
},
child: const Text('Load Warehouses'),
);
// Example 4: Refresh warehouses
RefreshIndicator(
onRefresh: () => ref.read(warehouseProvider.notifier).refresh(),
child: ListView(...),
);
*/