add dropdown
This commit is contained in:
@@ -34,6 +34,13 @@ class ApiEndpoints {
|
||||
/// GET: (requires auth token)
|
||||
static const String profile = '$apiVersion/auth/profile';
|
||||
|
||||
// ==================== User Endpoints ====================
|
||||
|
||||
/// Get all users (short info)
|
||||
/// GET: /PortalUser/GetAllMemberUserShortInfo (requires auth token)
|
||||
/// Response: List of users
|
||||
static const String users = '/PortalUser/GetAllMemberUserShortInfo';
|
||||
|
||||
// ==================== Warehouse Endpoints ====================
|
||||
|
||||
/// Get all warehouses
|
||||
|
||||
@@ -18,6 +18,14 @@ import '../../features/warehouse/data/repositories/warehouse_repository_impl.dar
|
||||
import '../../features/warehouse/domain/repositories/warehouse_repository.dart';
|
||||
import '../../features/warehouse/domain/usecases/get_warehouses_usecase.dart';
|
||||
import '../../features/warehouse/presentation/providers/warehouse_provider.dart';
|
||||
import '../../features/users/data/datasources/users_local_datasource.dart';
|
||||
import '../../features/users/data/datasources/users_remote_datasource.dart';
|
||||
import '../../features/users/data/repositories/users_repository_impl.dart';
|
||||
import '../../features/users/domain/repositories/users_repository.dart';
|
||||
import '../../features/users/domain/usecases/get_users_usecase.dart';
|
||||
import '../../features/users/domain/usecases/sync_users_usecase.dart';
|
||||
import '../../features/users/presentation/providers/users_provider.dart';
|
||||
import '../../features/users/presentation/providers/users_state.dart';
|
||||
import '../network/api_client.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
@@ -392,6 +400,105 @@ final productDetailErrorProvider = Provider.family<String?, String>((ref, key) {
|
||||
return state.error;
|
||||
});
|
||||
|
||||
/// ========================================================================
|
||||
/// USERS FEATURE PROVIDERS
|
||||
/// ========================================================================
|
||||
/// Providers for users feature following clean architecture
|
||||
|
||||
// Data Layer
|
||||
|
||||
/// Users local data source provider
|
||||
/// Handles local storage operations for users using Hive
|
||||
final usersLocalDataSourceProvider = Provider<UsersLocalDataSource>((ref) {
|
||||
return UsersLocalDataSourceImpl();
|
||||
});
|
||||
|
||||
/// Users remote data source provider
|
||||
/// Handles API calls for users
|
||||
final usersRemoteDataSourceProvider = Provider<UsersRemoteDataSource>((ref) {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
return UsersRemoteDataSourceImpl(apiClient);
|
||||
});
|
||||
|
||||
/// Users repository provider
|
||||
/// Implements domain repository interface
|
||||
/// Coordinates between local and remote data sources
|
||||
final usersRepositoryProvider = Provider<UsersRepository>((ref) {
|
||||
final remoteDataSource = ref.watch(usersRemoteDataSourceProvider);
|
||||
final localDataSource = ref.watch(usersLocalDataSourceProvider);
|
||||
return UsersRepositoryImpl(
|
||||
remoteDataSource: remoteDataSource,
|
||||
localDataSource: localDataSource,
|
||||
);
|
||||
});
|
||||
|
||||
// Domain Layer
|
||||
|
||||
/// Get users use case provider
|
||||
/// Encapsulates user fetching business logic (local-first strategy)
|
||||
final getUsersUseCaseProvider = Provider<GetUsersUseCase>((ref) {
|
||||
final repository = ref.watch(usersRepositoryProvider);
|
||||
return GetUsersUseCase(repository);
|
||||
});
|
||||
|
||||
/// Sync users use case provider
|
||||
/// Encapsulates user syncing business logic (force refresh from API)
|
||||
final syncUsersUseCaseProvider = Provider<SyncUsersUseCase>((ref) {
|
||||
final repository = ref.watch(usersRepositoryProvider);
|
||||
return SyncUsersUseCase(repository);
|
||||
});
|
||||
|
||||
// Presentation Layer
|
||||
|
||||
/// Users state notifier provider
|
||||
/// Manages users state including list, loading, and errors
|
||||
final usersProvider = StateNotifierProvider<UsersNotifier, UsersState>((ref) {
|
||||
final getUsersUseCase = ref.watch(getUsersUseCaseProvider);
|
||||
final syncUsersUseCase = ref.watch(syncUsersUseCaseProvider);
|
||||
return UsersNotifier(
|
||||
getUsersUseCase: getUsersUseCase,
|
||||
syncUsersUseCase: syncUsersUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
/// Convenient providers for users state
|
||||
|
||||
/// Provider to get list of users
|
||||
/// Usage: ref.watch(usersListProvider)
|
||||
final usersListProvider = Provider((ref) {
|
||||
final usersState = ref.watch(usersProvider);
|
||||
return usersState.users;
|
||||
});
|
||||
|
||||
/// Provider to check if users are loading
|
||||
/// Usage: ref.watch(isUsersLoadingProvider)
|
||||
final isUsersLoadingProvider = Provider<bool>((ref) {
|
||||
final usersState = ref.watch(usersProvider);
|
||||
return usersState.isLoading;
|
||||
});
|
||||
|
||||
/// Provider to check if users list has items
|
||||
/// Usage: ref.watch(hasUsersProvider)
|
||||
final hasUsersProvider = Provider<bool>((ref) {
|
||||
final usersState = ref.watch(usersProvider);
|
||||
return usersState.users.isNotEmpty;
|
||||
});
|
||||
|
||||
/// Provider to get users count
|
||||
/// Usage: ref.watch(usersCountProvider)
|
||||
final usersCountProvider = Provider<int>((ref) {
|
||||
final usersState = ref.watch(usersProvider);
|
||||
return usersState.users.length;
|
||||
});
|
||||
|
||||
/// Provider to get users error
|
||||
/// Returns null if no error
|
||||
/// Usage: ref.watch(usersErrorProvider)
|
||||
final usersErrorProvider = Provider<String?>((ref) {
|
||||
final usersState = ref.watch(usersProvider);
|
||||
return usersState.error;
|
||||
});
|
||||
|
||||
/// ========================================================================
|
||||
/// USAGE EXAMPLES
|
||||
/// ========================================================================
|
||||
|
||||
@@ -111,3 +111,20 @@ curl 'https://dotnet.elidev.info:8157/ws/portalWareHouse/createProductWareHouse'
|
||||
-H 'Sec-Fetch-Site: same-site' \
|
||||
-H 'Priority: u=0' \
|
||||
--data-raw $'[{"TypeId":4,"ProductId":11,"StageId":3,"OrderId":null,"RecordDate":"2025-10-28T08:19:20.418Z","PassedQuantityWeight":0.5,"PassedQuantity":5,"IssuedQuantityWeight":0.1,"IssuedQuantity":1,"ResponsibleUserId":12043,"Description":"","ProductName":"Th\xe9p 435","ProductCode":"SCM435","StockPassedQuantityWeight":0,"StockPassedQuantity":0,"StockIssuedQuantity":0,"StockIssuedQuantityWeight":0,"ReceiverUserId":12120,"ActionTypeId":1,"WareHouseId":1,"ProductStageId":3,"IsConfirm":true}]'
|
||||
|
||||
#Get users
|
||||
curl --request GET \
|
||||
--url https://dotnet.elidev.info:8157/ws/PortalUser/GetAllMemberUserShortInfo \
|
||||
--compressed \
|
||||
--header 'Accept: application/json, text/plain, */*' \
|
||||
--header 'Accept-Encoding: gzip, deflate, br, zstd' \
|
||||
--header 'Accept-Language: en-US,en;q=0.5' \
|
||||
--header 'AccessToken: 1k5fXyQVXtGkfjwS4g2USldGyLSA7Zwa2jdj5tLe+3YfSDlk02aYgqsFh5xArkdL4N529x7IYJOGrLJJgLBPNVgD51zFfEYBzfJmMH2RUm7iegvDJaMCLISySw0zd6kcsqeJi7vtuybgY2NDPxDgiSOj4wX417PzB8AVg5bl1ZAAJ3LcVAqqtA1PDTU5ZU1QQYapNeBNxAHjnd2ojTZK1GJBIyY5Gd8P9gB880ppAKq8manNMZYsa4d8tkYf0SJUul2aqLIWJAwDGORpPmfjqkN4hMh85xAfPTZi6m4DdI0u2rHDMLaZ8eIsV16qA8wimSDnWi0VeG0SZ4ugbCdJAi3t1/uICTftiy06PJEkulBLV+h2xS/7SlmEY2xoN5ISi++3FNqsFPGa9QH6akGu2C7IXEUBCg3iGJx0uL+vULmVqk5OJIXdqiKVQ366hvhPlK2AM1zbh49x/ngibe08483WTL5uAY/fsKuBxQCpTc2368Gqhpd7QRtZFKpzikhyTWsR3nQIi6ExSstCeFbe8ehgo0PuTPZNHH5IHTc49snH6IZrSbR+F62Wu/D+4DlvMTK/ktG6LVQ3r3jSJC5MAQDV5Q9WK3RvsWMPvZrsaVW/Exz0GBgWP4W0adADg7MFSlnGDOJm6I4fCLHZIJCUww50L6iNmzvrdibrQT5jKACVgNquMZCfeZlf3m2BwUx9T6J45lAePpJ+QaMh+2voFqRiOLi98MLqOG6TW7z96sadzFVR9YU1xwM51jQDjnUlrXt0+msq29Jqt8LoCyQsG4r3RgS/tUJhximq11MDXsSrJm6RubpKWl/MnF3QxcLTSwFE/SrGoGhnJnH5ILxZdyMN4PJjELeq3g8V5nEEm6lE/WNyvRMskel+Ods3XQIvE6o8KblUmFeM1rOIBkJbUVX7Ghaj0RNpvar86fF85BEozLBcED2XGkDANhGivuhqyrDpEOYjCwuC0eOjdj92fxlTTyo33ioR3xKcYFVlgMTlRX26sQDayf8hsPIBoDQtMHGKFfC2BJx4ujKTxtead8uz7c+CrlJuTbUkk+bp+wEUvKW5TvlX8HXKJWVLm05qZ7KmSLpsp35Iih2tZbBU+g==' \
|
||||
--header 'AppID: Minhthu2016' \
|
||||
--header 'Connection: keep-alive' \
|
||||
--header 'Origin: https://dotnet.elidev.info:8158' \
|
||||
--header 'Referer: https://dotnet.elidev.info:8158/' \
|
||||
--header 'Sec-Fetch-Dest: empty' \
|
||||
--header 'Sec-Fetch-Mode: cors' \
|
||||
--header 'Sec-Fetch-Site: same-site' \
|
||||
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0'
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/di/providers.dart';
|
||||
import '../../../users/domain/entities/user_entity.dart';
|
||||
import '../../domain/entities/product_stage_entity.dart';
|
||||
|
||||
/// Product detail page
|
||||
@@ -34,13 +35,20 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
final TextEditingController _issuedQuantityController = TextEditingController();
|
||||
final TextEditingController _issuedWeightController = TextEditingController();
|
||||
|
||||
// Selected users for dropdowns
|
||||
UserEntity? _selectedWarehouseUser;
|
||||
UserEntity? _selectedEmployee;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_providerKey = '${widget.warehouseId}_${widget.productId}';
|
||||
|
||||
// Load product stages when page is initialized
|
||||
// Load product stages and users when page is initialized
|
||||
Future.microtask(() async {
|
||||
// Load users from Hive (no API call)
|
||||
await ref.read(usersProvider.notifier).getUsers();
|
||||
|
||||
await ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail(
|
||||
widget.warehouseId,
|
||||
widget.productId,
|
||||
@@ -80,6 +88,10 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
_passedWeightController.clear();
|
||||
_issuedQuantityController.clear();
|
||||
_issuedWeightController.clear();
|
||||
setState(() {
|
||||
_selectedWarehouseUser = null;
|
||||
_selectedEmployee = null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -385,7 +397,6 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
'Passed Weight',
|
||||
'${stageToShow.passedQuantityWeight.toStringAsFixed(2)} kg',
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildInfoRow('Issued Quantity', '${stageToShow.issuedQuantity}'),
|
||||
_buildInfoRow(
|
||||
'Issued Weight',
|
||||
@@ -406,21 +417,18 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
keyboardType: TextInputType.number,
|
||||
theme: theme,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
label: 'Passed Weight (kg)',
|
||||
controller: _passedWeightController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
theme: theme,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildTextField(
|
||||
label: 'Issued Quantity',
|
||||
controller: _issuedQuantityController,
|
||||
keyboardType: TextInputType.number,
|
||||
theme: theme,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
label: 'Issued Weight (kg)',
|
||||
controller: _issuedWeightController,
|
||||
@@ -430,9 +438,34 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
],
|
||||
),
|
||||
|
||||
// Stage information
|
||||
|
||||
|
||||
_buildSectionCard(theme: theme, title: "Nhân viên", icon: Icons.people, children: [
|
||||
// Warehouse User Dropdown
|
||||
_buildUserDropdown(
|
||||
label: 'Warehouse User',
|
||||
value: _selectedWarehouseUser,
|
||||
users: ref.watch(usersListProvider)
|
||||
.where((user) => user.isWareHouseUser)
|
||||
.toList(),
|
||||
onChanged: (user) {
|
||||
setState(() {
|
||||
_selectedWarehouseUser = user;
|
||||
});
|
||||
},
|
||||
theme: theme,
|
||||
),
|
||||
// All Employees Dropdown
|
||||
_buildUserDropdown(
|
||||
label: 'Employee',
|
||||
value: _selectedEmployee,
|
||||
users: ref.watch(usersListProvider),
|
||||
onChanged: (user) {
|
||||
setState(() {
|
||||
_selectedEmployee = user;
|
||||
});
|
||||
},
|
||||
theme: theme,
|
||||
),
|
||||
]),
|
||||
|
||||
// Add button
|
||||
SizedBox(
|
||||
@@ -489,6 +522,10 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
|
||||
// Log the values for debugging
|
||||
debugPrint('Adding new quantities for stage ${stage.productStageId}:');
|
||||
debugPrint(' Warehouse User: ${_selectedWarehouseUser?.fullName ?? "Not selected"}');
|
||||
debugPrint(' Warehouse User ID: ${_selectedWarehouseUser?.id}');
|
||||
debugPrint(' Employee: ${_selectedEmployee?.fullName ?? "Not selected"}');
|
||||
debugPrint(' Employee ID: ${_selectedEmployee?.id}');
|
||||
debugPrint(' Passed Quantity: $passedQuantity');
|
||||
debugPrint(' Passed Weight: $passedWeight');
|
||||
debugPrint(' Issued Quantity: $issuedQuantity');
|
||||
@@ -556,6 +593,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 12,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
@@ -583,7 +621,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -699,6 +737,60 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserDropdown({
|
||||
required String label,
|
||||
required UserEntity? value,
|
||||
required List<UserEntity> users,
|
||||
required Function(UserEntity?) onChanged,
|
||||
required ThemeData theme,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonFormField<UserEntity>(
|
||||
value: value,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
hint: Text('Select $label'),
|
||||
items: users.map((user) {
|
||||
return DropdownMenuItem<UserEntity>(
|
||||
value: user,
|
||||
child: Text(
|
||||
user.name.isNotEmpty ? '${user.name} ${user.firstName}' : user.email,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: onChanged,
|
||||
isExpanded: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _incrementValue(TextEditingController controller, double increment) {
|
||||
final currentValue = double.tryParse(controller.text) ?? 0.0;
|
||||
final newValue = currentValue + increment;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
import '../models/user_model.dart';
|
||||
|
||||
/// Abstract interface for users local data source
|
||||
abstract class UsersLocalDataSource {
|
||||
/// Get all users from local storage
|
||||
Future<List<UserModel>> getUsers();
|
||||
|
||||
/// Save users to local storage
|
||||
Future<void> saveUsers(List<UserModel> users);
|
||||
|
||||
/// Clear all users from local storage
|
||||
Future<void> clearUsers();
|
||||
}
|
||||
|
||||
/// Implementation of UsersLocalDataSource using Hive
|
||||
class UsersLocalDataSourceImpl implements UsersLocalDataSource {
|
||||
static const String _boxName = 'users';
|
||||
|
||||
Future<Box<UserModel>> get _box async {
|
||||
if (!Hive.isBoxOpen(_boxName)) {
|
||||
return await Hive.openBox<UserModel>(_boxName);
|
||||
}
|
||||
return Hive.box<UserModel>(_boxName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<UserModel>> getUsers() async {
|
||||
try {
|
||||
final box = await _box;
|
||||
return box.values.toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get users from local storage: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveUsers(List<UserModel> users) async {
|
||||
try {
|
||||
final box = await _box;
|
||||
await box.clear();
|
||||
|
||||
// Save users with their ID as key
|
||||
for (final user in users) {
|
||||
await box.put(user.id, user);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to save users to local storage: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearUsers() async {
|
||||
try {
|
||||
final box = await _box;
|
||||
await box.clear();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to clear users from local storage: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import '../../../../core/constants/api_endpoints.dart';
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
import '../../../../core/network/api_response.dart';
|
||||
import '../models/user_model.dart';
|
||||
|
||||
/// Abstract interface for users remote data source
|
||||
abstract class UsersRemoteDataSource {
|
||||
/// Fetch all users from the API
|
||||
Future<List<UserModel>> getUsers();
|
||||
}
|
||||
|
||||
/// Implementation of UsersRemoteDataSource using ApiClient
|
||||
class UsersRemoteDataSourceImpl implements UsersRemoteDataSource {
|
||||
final ApiClient apiClient;
|
||||
|
||||
UsersRemoteDataSourceImpl(this.apiClient);
|
||||
|
||||
@override
|
||||
Future<List<UserModel>> getUsers() async {
|
||||
try {
|
||||
// Make API call to get all users
|
||||
final response = await apiClient.get(ApiEndpoints.users);
|
||||
|
||||
// Parse the API response using ApiResponse wrapper
|
||||
final apiResponse = ApiResponse.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
(json) => (json as List)
|
||||
.map((e) => UserModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
// Check if the API call was successful
|
||||
if (apiResponse.isSuccess && apiResponse.value != null) {
|
||||
return apiResponse.value!;
|
||||
} else {
|
||||
// Throw exception with error message from API
|
||||
throw ServerException(
|
||||
apiResponse.errors.isNotEmpty
|
||||
? apiResponse.errors.first
|
||||
: 'Failed to get users',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Re-throw ServerException as-is
|
||||
if (e is ServerException) {
|
||||
rethrow;
|
||||
}
|
||||
// Re-throw NetworkException as-is
|
||||
if (e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
// Wrap other exceptions in ServerException
|
||||
throw ServerException('Failed to get users: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
163
lib/features/users/data/models/user_model.dart
Normal file
163
lib/features/users/data/models/user_model.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
|
||||
part 'user_model.g.dart';
|
||||
|
||||
@HiveType(typeId: 1)
|
||||
class UserModel extends UserEntity {
|
||||
@HiveField(0)
|
||||
@override
|
||||
final int id;
|
||||
|
||||
@HiveField(1)
|
||||
@override
|
||||
final String firstName;
|
||||
|
||||
@HiveField(2)
|
||||
@override
|
||||
final String name;
|
||||
|
||||
@HiveField(3)
|
||||
@override
|
||||
final String? plateNumber;
|
||||
|
||||
@HiveField(4)
|
||||
@override
|
||||
final String email;
|
||||
|
||||
@HiveField(5)
|
||||
@override
|
||||
final String phone;
|
||||
|
||||
@HiveField(6)
|
||||
@override
|
||||
final bool isParent;
|
||||
|
||||
@HiveField(7)
|
||||
@override
|
||||
final String fullName;
|
||||
|
||||
@HiveField(8)
|
||||
@override
|
||||
final String fullNameEmail;
|
||||
|
||||
@HiveField(9)
|
||||
@override
|
||||
final String? referralCode;
|
||||
|
||||
@HiveField(10)
|
||||
@override
|
||||
final String? avatar;
|
||||
|
||||
@HiveField(11)
|
||||
@override
|
||||
final int departmentId;
|
||||
|
||||
@HiveField(12)
|
||||
@override
|
||||
final bool isWareHouseUser;
|
||||
|
||||
@HiveField(13)
|
||||
@override
|
||||
final int? wareHouseId;
|
||||
|
||||
@HiveField(14)
|
||||
@override
|
||||
final int roleId;
|
||||
|
||||
const UserModel({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.name,
|
||||
this.plateNumber,
|
||||
required this.email,
|
||||
required this.phone,
|
||||
this.isParent = false,
|
||||
required this.fullName,
|
||||
required this.fullNameEmail,
|
||||
this.referralCode,
|
||||
this.avatar,
|
||||
required this.departmentId,
|
||||
this.isWareHouseUser = false,
|
||||
this.wareHouseId,
|
||||
required this.roleId,
|
||||
}) : super(
|
||||
id: id,
|
||||
firstName: firstName,
|
||||
name: name,
|
||||
plateNumber: plateNumber,
|
||||
email: email,
|
||||
phone: phone,
|
||||
isParent: isParent,
|
||||
fullName: fullName,
|
||||
fullNameEmail: fullNameEmail,
|
||||
referralCode: referralCode,
|
||||
avatar: avatar,
|
||||
departmentId: departmentId,
|
||||
isWareHouseUser: isWareHouseUser,
|
||||
wareHouseId: wareHouseId,
|
||||
roleId: roleId,
|
||||
);
|
||||
|
||||
factory UserModel.fromJson(Map<String, dynamic> json) {
|
||||
return UserModel(
|
||||
id: json['Id'] ?? 0,
|
||||
firstName: json['FirstName'] ?? '',
|
||||
name: json['Name'] ?? '',
|
||||
plateNumber: json['PlateNumber'],
|
||||
email: json['Email'] ?? '',
|
||||
phone: json['Phone'] ?? '',
|
||||
isParent: json['IsParent'] ?? false,
|
||||
fullName: json['FullName'] ?? '',
|
||||
fullNameEmail: json['FullNameEmail'] ?? '',
|
||||
referralCode: json['ReferralCode'],
|
||||
avatar: json['Avatar'],
|
||||
departmentId: json['DepartmentId'] ?? 0,
|
||||
isWareHouseUser: json['IsWareHouseUser'] ?? false,
|
||||
wareHouseId: json['WareHouseId'],
|
||||
roleId: json['RoleId'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'Id': id,
|
||||
'FirstName': firstName,
|
||||
'Name': name,
|
||||
'PlateNumber': plateNumber,
|
||||
'Email': email,
|
||||
'Phone': phone,
|
||||
'IsParent': isParent,
|
||||
'FullName': fullName,
|
||||
'FullNameEmail': fullNameEmail,
|
||||
'ReferralCode': referralCode,
|
||||
'Avatar': avatar,
|
||||
'DepartmentId': departmentId,
|
||||
'IsWareHouseUser': isWareHouseUser,
|
||||
'WareHouseId': wareHouseId,
|
||||
'RoleId': roleId,
|
||||
};
|
||||
}
|
||||
|
||||
UserEntity toEntity() {
|
||||
return UserEntity(
|
||||
id: id,
|
||||
firstName: firstName,
|
||||
name: name,
|
||||
plateNumber: plateNumber,
|
||||
email: email,
|
||||
phone: phone,
|
||||
isParent: isParent,
|
||||
fullName: fullName,
|
||||
fullNameEmail: fullNameEmail,
|
||||
referralCode: referralCode,
|
||||
avatar: avatar,
|
||||
departmentId: departmentId,
|
||||
isWareHouseUser: isWareHouseUser,
|
||||
wareHouseId: wareHouseId,
|
||||
roleId: roleId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
83
lib/features/users/data/models/user_model.g.dart
Normal file
83
lib/features/users/data/models/user_model.g.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'user_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class UserModelAdapter extends TypeAdapter<UserModel> {
|
||||
@override
|
||||
final typeId = 1;
|
||||
|
||||
@override
|
||||
UserModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return UserModel(
|
||||
id: (fields[0] as num).toInt(),
|
||||
firstName: fields[1] as String,
|
||||
name: fields[2] as String,
|
||||
plateNumber: fields[3] as String?,
|
||||
email: fields[4] as String,
|
||||
phone: fields[5] as String,
|
||||
isParent: fields[6] == null ? false : fields[6] as bool,
|
||||
fullName: fields[7] as String,
|
||||
fullNameEmail: fields[8] as String,
|
||||
referralCode: fields[9] as String?,
|
||||
avatar: fields[10] as String?,
|
||||
departmentId: (fields[11] as num).toInt(),
|
||||
isWareHouseUser: fields[12] == null ? false : fields[12] as bool,
|
||||
wareHouseId: (fields[13] as num?)?.toInt(),
|
||||
roleId: (fields[14] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, UserModel obj) {
|
||||
writer
|
||||
..writeByte(15)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.firstName)
|
||||
..writeByte(2)
|
||||
..write(obj.name)
|
||||
..writeByte(3)
|
||||
..write(obj.plateNumber)
|
||||
..writeByte(4)
|
||||
..write(obj.email)
|
||||
..writeByte(5)
|
||||
..write(obj.phone)
|
||||
..writeByte(6)
|
||||
..write(obj.isParent)
|
||||
..writeByte(7)
|
||||
..write(obj.fullName)
|
||||
..writeByte(8)
|
||||
..write(obj.fullNameEmail)
|
||||
..writeByte(9)
|
||||
..write(obj.referralCode)
|
||||
..writeByte(10)
|
||||
..write(obj.avatar)
|
||||
..writeByte(11)
|
||||
..write(obj.departmentId)
|
||||
..writeByte(12)
|
||||
..write(obj.isWareHouseUser)
|
||||
..writeByte(13)
|
||||
..write(obj.wareHouseId)
|
||||
..writeByte(14)
|
||||
..write(obj.roleId);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is UserModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
import '../../domain/repositories/users_repository.dart';
|
||||
import '../datasources/users_local_datasource.dart';
|
||||
import '../datasources/users_remote_datasource.dart';
|
||||
|
||||
/// Implementation of UsersRepository
|
||||
class UsersRepositoryImpl implements UsersRepository {
|
||||
final UsersRemoteDataSource remoteDataSource;
|
||||
final UsersLocalDataSource localDataSource;
|
||||
|
||||
UsersRepositoryImpl({
|
||||
required this.remoteDataSource,
|
||||
required this.localDataSource,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<UserEntity>>> getUsers() async {
|
||||
try {
|
||||
// Try to get users from local storage first
|
||||
final localUsers = await localDataSource.getUsers();
|
||||
|
||||
if (localUsers.isNotEmpty) {
|
||||
// Return local users if available
|
||||
return Right(localUsers.map((model) => model.toEntity()).toList());
|
||||
}
|
||||
|
||||
// If no local users, fetch from API
|
||||
return await syncUsers();
|
||||
} catch (e) {
|
||||
return Left(CacheFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<UserEntity>>> syncUsers() async {
|
||||
try {
|
||||
// Fetch users from API
|
||||
final users = await remoteDataSource.getUsers();
|
||||
|
||||
// Save to local storage
|
||||
await localDataSource.saveUsers(users);
|
||||
|
||||
// Return as entities
|
||||
return Right(users.map((model) => model.toEntity()).toList());
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> clearUsers() async {
|
||||
try {
|
||||
await localDataSource.clearUsers();
|
||||
return const Right(null);
|
||||
} catch (e) {
|
||||
return Left(CacheFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
78
lib/features/users/domain/entities/user_entity.dart
Normal file
78
lib/features/users/domain/entities/user_entity.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// User entity representing a user in the system
|
||||
class UserEntity extends Equatable {
|
||||
final int id;
|
||||
final String firstName;
|
||||
final String name;
|
||||
final String? plateNumber;
|
||||
final String email;
|
||||
final String phone;
|
||||
final bool isParent;
|
||||
final String fullName;
|
||||
final String fullNameEmail;
|
||||
final String? referralCode;
|
||||
final String? avatar;
|
||||
final int departmentId;
|
||||
final bool isWareHouseUser;
|
||||
final int? wareHouseId;
|
||||
final int roleId;
|
||||
|
||||
const UserEntity({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.name,
|
||||
this.plateNumber,
|
||||
required this.email,
|
||||
required this.phone,
|
||||
this.isParent = false,
|
||||
required this.fullName,
|
||||
required this.fullNameEmail,
|
||||
this.referralCode,
|
||||
this.avatar,
|
||||
required this.departmentId,
|
||||
this.isWareHouseUser = false,
|
||||
this.wareHouseId,
|
||||
required this.roleId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
firstName,
|
||||
name,
|
||||
plateNumber,
|
||||
email,
|
||||
phone,
|
||||
isParent,
|
||||
fullName,
|
||||
fullNameEmail,
|
||||
referralCode,
|
||||
avatar,
|
||||
departmentId,
|
||||
isWareHouseUser,
|
||||
wareHouseId,
|
||||
roleId,
|
||||
];
|
||||
|
||||
/// Get display name
|
||||
String get displayName {
|
||||
if (fullName.isNotEmpty) return fullName;
|
||||
if (name.isNotEmpty) return name;
|
||||
return email;
|
||||
}
|
||||
|
||||
/// Get initials from name (for avatar display)
|
||||
String get initials {
|
||||
if (firstName.isNotEmpty && name.isNotEmpty) {
|
||||
return (firstName.substring(0, 1) + name.substring(0, 1)).toUpperCase();
|
||||
}
|
||||
final parts = fullName.trim().split(' ');
|
||||
if (parts.isEmpty) return '?';
|
||||
if (parts.length == 1) {
|
||||
return parts[0].substring(0, 1).toUpperCase();
|
||||
}
|
||||
return (parts[0].substring(0, 1) + parts[parts.length - 1].substring(0, 1))
|
||||
.toUpperCase();
|
||||
}
|
||||
}
|
||||
16
lib/features/users/domain/repositories/users_repository.dart
Normal file
16
lib/features/users/domain/repositories/users_repository.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/user_entity.dart';
|
||||
|
||||
/// Abstract repository interface for users
|
||||
abstract class UsersRepository {
|
||||
/// Get all users (from local storage if available, otherwise from API)
|
||||
Future<Either<Failure, List<UserEntity>>> getUsers();
|
||||
|
||||
/// Sync users from API and save to local storage
|
||||
Future<Either<Failure, List<UserEntity>>> syncUsers();
|
||||
|
||||
/// Clear all users from local storage
|
||||
Future<Either<Failure, void>> clearUsers();
|
||||
}
|
||||
18
lib/features/users/domain/usecases/get_users_usecase.dart
Normal file
18
lib/features/users/domain/usecases/get_users_usecase.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/user_entity.dart';
|
||||
import '../repositories/users_repository.dart';
|
||||
|
||||
/// Use case for getting users
|
||||
/// Follows local-first strategy: returns local users if available,
|
||||
/// otherwise syncs from API
|
||||
class GetUsersUseCase {
|
||||
final UsersRepository repository;
|
||||
|
||||
GetUsersUseCase(this.repository);
|
||||
|
||||
Future<Either<Failure, List<UserEntity>>> call() async {
|
||||
return await repository.getUsers();
|
||||
}
|
||||
}
|
||||
17
lib/features/users/domain/usecases/sync_users_usecase.dart
Normal file
17
lib/features/users/domain/usecases/sync_users_usecase.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/user_entity.dart';
|
||||
import '../repositories/users_repository.dart';
|
||||
|
||||
/// Use case for syncing users from API
|
||||
/// Forces refresh from API and saves to local storage
|
||||
class SyncUsersUseCase {
|
||||
final UsersRepository repository;
|
||||
|
||||
SyncUsersUseCase(this.repository);
|
||||
|
||||
Future<Either<Failure, List<UserEntity>>> call() async {
|
||||
return await repository.syncUsers();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../domain/usecases/get_users_usecase.dart';
|
||||
import '../../domain/usecases/sync_users_usecase.dart';
|
||||
import 'users_state.dart';
|
||||
|
||||
/// State notifier for users
|
||||
class UsersNotifier extends StateNotifier<UsersState> {
|
||||
final GetUsersUseCase getUsersUseCase;
|
||||
final SyncUsersUseCase syncUsersUseCase;
|
||||
|
||||
UsersNotifier({
|
||||
required this.getUsersUseCase,
|
||||
required this.syncUsersUseCase,
|
||||
}) : super(const UsersState());
|
||||
|
||||
/// Get users from local storage (or API if not cached)
|
||||
Future<void> getUsers() async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
final result = await getUsersUseCase();
|
||||
|
||||
result.fold(
|
||||
(failure) => state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: failure.message,
|
||||
),
|
||||
(users) => state = state.copyWith(
|
||||
users: users,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sync users from API (force refresh)
|
||||
Future<void> syncUsers() async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
final result = await syncUsersUseCase();
|
||||
|
||||
result.fold(
|
||||
(failure) => state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: failure.message,
|
||||
),
|
||||
(users) => state = state.copyWith(
|
||||
users: users,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
31
lib/features/users/presentation/providers/users_state.dart
Normal file
31
lib/features/users/presentation/providers/users_state.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
|
||||
/// State for users feature
|
||||
class UsersState extends Equatable {
|
||||
final List<UserEntity> users;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const UsersState({
|
||||
this.users = const [],
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
UsersState copyWith({
|
||||
List<UserEntity>? users,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
}) {
|
||||
return UsersState(
|
||||
users: users ?? this.users,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [users, isLoading, error];
|
||||
}
|
||||
@@ -26,9 +26,11 @@ class _WarehouseSelectionPageState
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Load warehouses when page is first created
|
||||
// Load warehouses and sync users when page is first created
|
||||
Future.microtask(() {
|
||||
ref.read(warehouseProvider.notifier).loadWarehouses();
|
||||
// Sync users from API and save to local storage
|
||||
ref.read(usersProvider.notifier).syncUsers();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
18
lib/hive_registrar.g.dart
Normal file
18
lib/hive_registrar.g.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
// Generated by Hive CE
|
||||
// Do not modify
|
||||
// Check in to version control
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:minhthu/features/users/data/models/user_model.dart';
|
||||
|
||||
extension HiveRegistrar on HiveInterface {
|
||||
void registerAdapters() {
|
||||
registerAdapter(UserModelAdapter());
|
||||
}
|
||||
}
|
||||
|
||||
extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
void registerAdapters() {
|
||||
registerAdapter(UserModelAdapter());
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'core/theme/app_theme.dart';
|
||||
import 'core/router/app_router.dart';
|
||||
import 'features/users/data/models/user_model.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Initialize Hive
|
||||
final appDocumentDir = await getApplicationDocumentsDirectory();
|
||||
await Hive.initFlutter(appDocumentDir.path);
|
||||
|
||||
// Register Hive adapters
|
||||
Hive.registerAdapter(UserModelAdapter());
|
||||
|
||||
runApp(
|
||||
const ProviderScope(
|
||||
child: MyApp(),
|
||||
|
||||
@@ -633,7 +633,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
|
||||
@@ -21,6 +21,7 @@ dependencies:
|
||||
dartz: ^0.10.1
|
||||
get_it: ^7.6.4
|
||||
flutter_secure_storage: ^9.0.0
|
||||
path_provider: ^2.1.0
|
||||
|
||||
# Data Classes & Serialization
|
||||
freezed_annotation: ^2.4.1
|
||||
|
||||
Reference in New Issue
Block a user