add dropdown

This commit is contained in:
Phuoc Nguyen
2025-10-28 16:24:17 +07:00
parent 0010446298
commit 5cfc56f40d
20 changed files with 912 additions and 12 deletions

View File

@@ -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');
}
}
}

View File

@@ -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()}');
}
}
}

View 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,
);
}
}

View 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;
}

View File

@@ -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()));
}
}
}

View 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();
}
}

View 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();
}

View 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();
}
}

View 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();
}
}

View File

@@ -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,
),
);
}
}

View 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];
}