Compare commits
9 Commits
1fff3b402c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd7ba8e9d5 | ||
|
|
33099c4497 | ||
|
|
79b53b6303 | ||
|
|
8adab4c4af | ||
|
|
2fd5aff0c2 | ||
|
|
56e2c0ffaa | ||
|
|
3b3ed5e640 | ||
|
|
7cbf65e6c1 | ||
|
|
cefd2397fe |
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "finger_print_app",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "finger_print_app (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "finger_print_app (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,19 +1,36 @@
|
||||
import 'package:coda_project/data/datasources/attendance_remote_data_source.dart';
|
||||
import 'package:coda_project/data/repositories/attendance_repository_impl.dart';
|
||||
import 'package:coda_project/domain/repositories/attendance_repository.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../network/api_client.dart';
|
||||
import '../../data/datasources/auth_remote_data_source.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
import '../../data/datasources/vacation_remote_data_source.dart';
|
||||
import '../../data/datasources/advance_remote_data_source.dart';
|
||||
import '../../data/repositories/auth_repository_impl.dart';
|
||||
import '../../data/repositories/vacation_repository_impl.dart';
|
||||
import '../../data/repositories/advance_repository_impl.dart';
|
||||
import '../../domain/repositories/auth_repository.dart';
|
||||
import '../../domain/repositories/vacation_repository.dart';
|
||||
import '../../domain/repositories/advance_repository.dart';
|
||||
import '../../domain/usecases/login_usecase.dart';
|
||||
import '../../domain/usecases/attendance_login_usecase.dart';
|
||||
import '../../domain/usecases/attendance_logout_usecase.dart';
|
||||
import '../../domain/usecases/create_vacation_usecase.dart';
|
||||
import '../../domain/usecases/get_vacation_types_usecase.dart';
|
||||
import '../../domain/usecases/get_vacations_usecase.dart';
|
||||
import '../../domain/usecases/create_advance_usecase.dart';
|
||||
import '../../domain/usecases/get_advances_usecase.dart';
|
||||
import '../../presentation/blocs/login/login_bloc.dart';
|
||||
|
||||
final sl = GetIt.instance;
|
||||
|
||||
Future<void> initializeDependencies() async {
|
||||
// External
|
||||
sl.registerLazySingleton<Dio>(() => Dio());
|
||||
|
||||
|
||||
// SharedPreferences
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
sl.registerLazySingleton<SharedPreferences>(() => sharedPreferences);
|
||||
@@ -27,23 +44,57 @@ Future<void> initializeDependencies() async {
|
||||
sl.registerLazySingleton<AuthRemoteDataSource>(
|
||||
() => AuthRemoteDataSourceImpl(apiClient: sl()),
|
||||
);
|
||||
|
||||
|
||||
sl.registerLazySingleton<UserLocalDataSource>(
|
||||
() => UserLocalDataSourceImpl(sharedPreferences: sl()),
|
||||
);
|
||||
|
||||
// Repositories
|
||||
sl.registerLazySingleton<AuthRepository>(
|
||||
() => AuthRepositoryImpl(
|
||||
remoteDataSource: sl(),
|
||||
localDataSource: sl(),
|
||||
),
|
||||
() => AuthRepositoryImpl(remoteDataSource: sl(), localDataSource: sl()),
|
||||
);
|
||||
|
||||
// Use cases
|
||||
sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
|
||||
|
||||
// Blocs will be registered here
|
||||
// Example:
|
||||
// sl.registerFactory(() => LoginBloc(loginUseCase: sl()));
|
||||
// Blocs
|
||||
sl.registerFactory(() => LoginBloc(loginUseCase: sl()));
|
||||
|
||||
//Attendence
|
||||
sl.registerLazySingleton<AttendanceRemoteDataSource>(
|
||||
() => AttendanceRemoteDataSourceImpl(apiClient: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<AttendanceRepository>(
|
||||
() => AttendanceRepositoryImpl(remoteDataSource: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton(() => AttendanceLoginUsecase(repository: sl()));
|
||||
|
||||
sl.registerLazySingleton(() => AttendanceLogoutUseCase(repository: sl()));
|
||||
|
||||
// Vacation
|
||||
sl.registerLazySingleton<VacationRemoteDataSource>(
|
||||
() => VacationRemoteDataSourceImpl(apiClient: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<VacationRepository>(
|
||||
() => VacationRepositoryImpl(remoteDataSource: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton(() => CreateVacationUseCase(repository: sl()));
|
||||
sl.registerLazySingleton(() => GetVacationTypesUseCase(repository: sl()));
|
||||
sl.registerLazySingleton(() => GetVacationsUseCase(repository: sl()));
|
||||
|
||||
// Advance
|
||||
sl.registerLazySingleton<AdvanceRemoteDataSource>(
|
||||
() => AdvanceRemoteDataSourceImpl(apiClient: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<AdvanceRepository>(
|
||||
() => AdvanceRepositoryImpl(remoteDataSource: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton(() => CreateAdvanceUseCase(repository: sl()));
|
||||
sl.registerLazySingleton(() => GetAdvancesUseCase(repository: sl()));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:async';
|
||||
import '../models/leave_request.dart';
|
||||
import '../models/advance_request.dart';
|
||||
import '../../models/leave_request.dart';
|
||||
import '../../models/advance_request.dart';
|
||||
|
||||
class RequestService {
|
||||
// Singleton implementation
|
||||
124
lib/data/datasources/advance_remote_data_source.dart
Normal file
124
lib/data/datasources/advance_remote_data_source.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../core/error/exceptions.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../dto/advance_request_dto.dart';
|
||||
import '../dto/advance_response_dto.dart';
|
||||
import '../dto/advances_list_response_dto.dart';
|
||||
|
||||
abstract class AdvanceRemoteDataSource {
|
||||
Future<AdvanceResponseDto> createAdvance(AdvanceRequestDto request);
|
||||
Future<AdvancesListResponseDto> getAdvances();
|
||||
}
|
||||
|
||||
class AdvanceRemoteDataSourceImpl implements AdvanceRemoteDataSource {
|
||||
final ApiClient apiClient;
|
||||
|
||||
AdvanceRemoteDataSourceImpl({required this.apiClient});
|
||||
|
||||
@override
|
||||
Future<AdvanceResponseDto> createAdvance(AdvanceRequestDto request) async {
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/SalaryInAdvance',
|
||||
data: request.toJson(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return AdvanceResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل إنشاء طلب السلفة',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل إنشاء طلب السلفة';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AdvancesListResponseDto> getAdvances() async {
|
||||
try {
|
||||
final response = await apiClient.get('/SalaryInAdvance');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return AdvancesListResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل جلب قائمة السلف',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل جلب قائمة السلف';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
}
|
||||
149
lib/data/datasources/attendance_remote_data_source.dart
Normal file
149
lib/data/datasources/attendance_remote_data_source.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../core/error/exceptions.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../dto/attendance_response_dto.dart';
|
||||
|
||||
abstract class AttendanceRemoteDataSource {
|
||||
Future<AttendanceResponseDto> login({
|
||||
required String employeeId,
|
||||
required File faceImage,
|
||||
});
|
||||
|
||||
Future<AttendanceResponseDto> logout({
|
||||
required String employeeId,
|
||||
required File faceImage,
|
||||
});
|
||||
}
|
||||
|
||||
class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
||||
final ApiClient apiClient;
|
||||
|
||||
AttendanceRemoteDataSourceImpl({required this.apiClient});
|
||||
@override
|
||||
Future<AttendanceResponseDto> login({
|
||||
required String employeeId,
|
||||
required File faceImage,
|
||||
}) async {
|
||||
try {
|
||||
final formData = FormData.fromMap({
|
||||
'EmployeeId': employeeId,
|
||||
'FaceImage': await MultipartFile.fromFile(faceImage.path),
|
||||
});
|
||||
|
||||
final response = await apiClient.post(
|
||||
'/Attendance/login',
|
||||
data: formData,
|
||||
options: Options(contentType: 'multipart/form-data'),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return AttendanceResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل تسجيل الدخول',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل تسجيل الدخول';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AttendanceResponseDto> logout({
|
||||
required String employeeId,
|
||||
required File faceImage,
|
||||
}) async {
|
||||
try {
|
||||
final formData = FormData.fromMap({
|
||||
'EmployeeId': employeeId,
|
||||
'FaceImage': await MultipartFile.fromFile(faceImage.path),
|
||||
});
|
||||
|
||||
final response = await apiClient.post(
|
||||
'/Attendance/logout',
|
||||
data: formData,
|
||||
options: Options(contentType: 'multipart/form-data'),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return AttendanceResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل تسجيل الخروج',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل تسجيل الخروج';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,7 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||
@override
|
||||
Future<LoginResponseDto> login(LoginDto dto) async {
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/Auth/login',
|
||||
data: dto.toJson(),
|
||||
);
|
||||
final response = await apiClient.post('/Auth/login', data: dto.toJson());
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final responseData = response.data;
|
||||
@@ -47,7 +44,8 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message = e.response?.data?['message'] ??
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل تسجيل الدخول';
|
||||
|
||||
@@ -57,8 +55,8 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||
message.toString().toLowerCase().contains('incorrect')
|
||||
? 'رقم الهاتف أو كلمة المرور غير صحيحة'
|
||||
: message.toString().toLowerCase().contains('not found')
|
||||
? 'المستخدم غير موجود'
|
||||
: message;
|
||||
? 'المستخدم غير موجود'
|
||||
: message;
|
||||
|
||||
throw ServerException(
|
||||
message: customMessage,
|
||||
|
||||
@@ -4,11 +4,14 @@ abstract class UserLocalDataSource {
|
||||
Future<void> cacheUserToken(String token);
|
||||
Future<String?> getCachedUserToken();
|
||||
Future<void> clearCache();
|
||||
Future<void> cacheEmployeeId(String id);
|
||||
Future<String?> getCachedEmployeeId();
|
||||
}
|
||||
|
||||
class UserLocalDataSourceImpl implements UserLocalDataSource {
|
||||
final SharedPreferences sharedPreferences;
|
||||
static const String _tokenKey = 'user_token';
|
||||
static const String _employeeIdKey = 'employee_id';
|
||||
|
||||
UserLocalDataSourceImpl({required this.sharedPreferences});
|
||||
|
||||
@@ -25,5 +28,16 @@ class UserLocalDataSourceImpl implements UserLocalDataSource {
|
||||
@override
|
||||
Future<void> clearCache() async {
|
||||
await sharedPreferences.remove(_tokenKey);
|
||||
await sharedPreferences.remove(_employeeIdKey);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cacheEmployeeId(String id) async {
|
||||
await sharedPreferences.setString(_employeeIdKey, id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getCachedEmployeeId() async {
|
||||
return sharedPreferences.getString(_employeeIdKey);
|
||||
}
|
||||
}
|
||||
|
||||
178
lib/data/datasources/vacation_remote_data_source.dart
Normal file
178
lib/data/datasources/vacation_remote_data_source.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../core/error/exceptions.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../dto/vacation_request_dto.dart';
|
||||
import '../dto/vacation_response_dto.dart';
|
||||
import '../dto/vacation_type_dto.dart';
|
||||
import '../dto/vacations_list_response_dto.dart';
|
||||
|
||||
abstract class VacationRemoteDataSource {
|
||||
Future<VacationResponseDto> createVacation(VacationRequestDto request);
|
||||
Future<VacationTypesResponseDto> getVacationTypes();
|
||||
Future<VacationsListResponseDto> getVacations();
|
||||
}
|
||||
|
||||
class VacationRemoteDataSourceImpl implements VacationRemoteDataSource {
|
||||
final ApiClient apiClient;
|
||||
|
||||
VacationRemoteDataSourceImpl({required this.apiClient});
|
||||
|
||||
@override
|
||||
Future<VacationResponseDto> createVacation(VacationRequestDto request) async {
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/Vacation',
|
||||
data: request.toJson(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return VacationResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل إنشاء طلب الإجازة',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل إنشاء طلب الإجازة';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<VacationTypesResponseDto> getVacationTypes() async {
|
||||
try {
|
||||
final response = await apiClient.get('/enums/vacation-types');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return VacationTypesResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل جلب أنواع الإجازات',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل جلب أنواع الإجازات';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<VacationsListResponseDto> getVacations() async {
|
||||
try {
|
||||
final response = await apiClient.get('/Vacation');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return VacationsListResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل جلب قائمة الإجازات',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل جلب قائمة الإجازات';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
}
|
||||
22
lib/data/dto/advance_request_dto.dart
Normal file
22
lib/data/dto/advance_request_dto.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
class AdvanceRequestDto {
|
||||
final String employeeId;
|
||||
final DateTime date;
|
||||
final double amount;
|
||||
final String reason;
|
||||
|
||||
AdvanceRequestDto({
|
||||
required this.employeeId,
|
||||
required this.date,
|
||||
required this.amount,
|
||||
required this.reason,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'employeeId': employeeId,
|
||||
'date': date.toIso8601String(),
|
||||
'amount': amount,
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
}
|
||||
86
lib/data/dto/advance_response_dto.dart
Normal file
86
lib/data/dto/advance_response_dto.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
class AdvanceResponseDto {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final AdvanceDataDto? data;
|
||||
|
||||
AdvanceResponseDto({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory AdvanceResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
return AdvanceResponseDto(
|
||||
statusCode: json['statusCode'] ?? 0,
|
||||
isSuccess: json['isSuccess'] ?? false,
|
||||
message: json['message'],
|
||||
data: json['data'] != null ? AdvanceDataDto.fromJson(json['data']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AdvanceDataDto {
|
||||
final String employeeId;
|
||||
final String? employeeFullName;
|
||||
final DateTime date;
|
||||
final double amount;
|
||||
final String? submittedBy;
|
||||
final String? submittedByUser;
|
||||
final String reason;
|
||||
final int state;
|
||||
final String id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final bool? isDeleted;
|
||||
|
||||
AdvanceDataDto({
|
||||
required this.employeeId,
|
||||
this.employeeFullName,
|
||||
required this.date,
|
||||
required this.amount,
|
||||
this.submittedBy,
|
||||
this.submittedByUser,
|
||||
required this.reason,
|
||||
required this.state,
|
||||
required this.id,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.isDeleted,
|
||||
});
|
||||
|
||||
factory AdvanceDataDto.fromJson(Map<String, dynamic> json) {
|
||||
return AdvanceDataDto(
|
||||
employeeId: json['employeeId']?.toString() ?? '',
|
||||
employeeFullName: json['employeeFullName'],
|
||||
date: _parseDateTime(json['date'])!,
|
||||
amount: (json['amount'] is int) ? (json['amount'] as int).toDouble() : (json['amount'] as num).toDouble(),
|
||||
submittedBy: json['submittedBy'],
|
||||
submittedByUser: json['submittedByUser'],
|
||||
reason: json['reason']?.toString() ?? '',
|
||||
state: json['state'] ?? 0,
|
||||
id: json['id']?.toString() ?? '',
|
||||
createdAt: _parseDateTime(json['createdAt']),
|
||||
updatedAt: _parseDateTime(json['updatedAt']),
|
||||
deletedAt: _parseDateTime(json['deletedAt']),
|
||||
isDeleted: json['isDeleted'],
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
if (value is String) {
|
||||
try {
|
||||
return DateTime.parse(value);
|
||||
} catch (e) {
|
||||
print('Error parsing date: $value - $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
54
lib/data/dto/advances_list_response_dto.dart
Normal file
54
lib/data/dto/advances_list_response_dto.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'advance_response_dto.dart';
|
||||
|
||||
class AdvancesListResponseDto {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final AdvancesListDataDto? data;
|
||||
|
||||
AdvancesListResponseDto({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory AdvancesListResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
return AdvancesListResponseDto(
|
||||
statusCode: json['statusCode'] ?? 0,
|
||||
isSuccess: json['isSuccess'] ?? false,
|
||||
message: json['message'],
|
||||
data: json['data'] != null ? AdvancesListDataDto.fromJson(json['data']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AdvancesListDataDto {
|
||||
final List<AdvanceDataDto> items;
|
||||
final int pageNumber;
|
||||
final int pageSize;
|
||||
final int totalCount;
|
||||
final int totalPages;
|
||||
|
||||
AdvancesListDataDto({
|
||||
required this.items,
|
||||
required this.pageNumber,
|
||||
required this.pageSize,
|
||||
required this.totalCount,
|
||||
required this.totalPages,
|
||||
});
|
||||
|
||||
factory AdvancesListDataDto.fromJson(Map<String, dynamic> json) {
|
||||
return AdvancesListDataDto(
|
||||
items: json['items'] != null
|
||||
? (json['items'] as List)
|
||||
.map((item) => AdvanceDataDto.fromJson(item))
|
||||
.toList()
|
||||
: [],
|
||||
pageNumber: json['pageNumber'] ?? 1,
|
||||
pageSize: json['pageSize'] ?? 15,
|
||||
totalCount: json['totalCount'] ?? 0,
|
||||
totalPages: json['totalPages'] ?? 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib/data/dto/attendance_response_dto.dart
Normal file
38
lib/data/dto/attendance_response_dto.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
class AttendanceResponseDto {
|
||||
final String id;
|
||||
final String employeeId;
|
||||
final DateTime? login;
|
||||
final DateTime? logout;
|
||||
|
||||
AttendanceResponseDto({
|
||||
required this.id,
|
||||
required this.employeeId,
|
||||
this.login,
|
||||
this.logout,
|
||||
});
|
||||
|
||||
factory AttendanceResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
final data = json['data'];
|
||||
|
||||
return AttendanceResponseDto(
|
||||
id: data['id']?.toString() ?? '',
|
||||
employeeId: data['employeeId']?.toString() ?? '',
|
||||
login: _parseDateTime(data['login']),
|
||||
logout: _parseDateTime(data['logout']),
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
if (value is String) {
|
||||
try {
|
||||
return DateTime.parse(value);
|
||||
} catch (e) {
|
||||
print('Error parsing date: $value - $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -57,15 +57,17 @@ class LoginDataDto {
|
||||
return LoginDataDto(
|
||||
token: json['token'],
|
||||
id: json['id'],
|
||||
employeeId: json['employeeId'],
|
||||
employeeId:
|
||||
json['employeeId'] ?? json['EmployeeId'] ?? json['employee_id'],
|
||||
username: json['username'],
|
||||
fullName: json['fullName'],
|
||||
role: json['role'],
|
||||
email: json['email'],
|
||||
phoneNumber: json['phoneNumber'],
|
||||
permissions: json['permissions'] != null
|
||||
? List<String>.from(json['permissions'])
|
||||
: null,
|
||||
permissions:
|
||||
json['permissions'] != null
|
||||
? List<String>.from(json['permissions'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
25
lib/data/dto/vacation_request_dto.dart
Normal file
25
lib/data/dto/vacation_request_dto.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
class VacationRequestDto {
|
||||
final String employeeId;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final String reason;
|
||||
final int type;
|
||||
|
||||
VacationRequestDto({
|
||||
required this.employeeId,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.reason,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'employeeId': employeeId,
|
||||
'startDate': startDate.toIso8601String(),
|
||||
'endDate': endDate.toIso8601String(),
|
||||
'reason': reason,
|
||||
'type': type,
|
||||
};
|
||||
}
|
||||
}
|
||||
89
lib/data/dto/vacation_response_dto.dart
Normal file
89
lib/data/dto/vacation_response_dto.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
class VacationResponseDto {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String message;
|
||||
final VacationDataDto? data;
|
||||
|
||||
VacationResponseDto({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
required this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory VacationResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationResponseDto(
|
||||
statusCode: json['statusCode'] ?? 0,
|
||||
isSuccess: json['isSuccess'] ?? false,
|
||||
message: json['message'] ?? '',
|
||||
data: json['data'] != null ? VacationDataDto.fromJson(json['data']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VacationDataDto {
|
||||
final String employeeId;
|
||||
final String? employeeFullName;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final String reason;
|
||||
final String? submittedBy;
|
||||
final String? submittedByUser;
|
||||
final int state;
|
||||
final int type;
|
||||
final String id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final bool? isDeleted;
|
||||
|
||||
VacationDataDto({
|
||||
required this.employeeId,
|
||||
this.employeeFullName,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.reason,
|
||||
this.submittedBy,
|
||||
this.submittedByUser,
|
||||
required this.state,
|
||||
required this.type,
|
||||
required this.id,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.isDeleted,
|
||||
});
|
||||
|
||||
factory VacationDataDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationDataDto(
|
||||
employeeId: json['employeeId']?.toString() ?? '',
|
||||
employeeFullName: json['employeeFullName'],
|
||||
startDate: _parseDateTime(json['startDate'])!,
|
||||
endDate: _parseDateTime(json['endDate'])!,
|
||||
reason: json['reason']?.toString() ?? '',
|
||||
submittedBy: json['submittedBy'],
|
||||
submittedByUser: json['submittedByUser'],
|
||||
state: json['state'] ?? 0,
|
||||
type: json['type'] ?? 0,
|
||||
id: json['id']?.toString() ?? '',
|
||||
createdAt: _parseDateTime(json['createdAt']),
|
||||
updatedAt: _parseDateTime(json['updatedAt']),
|
||||
deletedAt: _parseDateTime(json['deletedAt']),
|
||||
isDeleted: json['isDeleted'],
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
if (value is String) {
|
||||
try {
|
||||
return DateTime.parse(value);
|
||||
} catch (e) {
|
||||
print('Error parsing date: $value - $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
43
lib/data/dto/vacation_type_dto.dart
Normal file
43
lib/data/dto/vacation_type_dto.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
class VacationTypesResponseDto {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final List<VacationTypeDto> data;
|
||||
|
||||
VacationTypesResponseDto({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
factory VacationTypesResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationTypesResponseDto(
|
||||
statusCode: json['statusCode'] ?? 0,
|
||||
isSuccess: json['isSuccess'] ?? false,
|
||||
message: json['message'],
|
||||
data: json['data'] != null
|
||||
? (json['data'] as List)
|
||||
.map((item) => VacationTypeDto.fromJson(item))
|
||||
.toList()
|
||||
: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VacationTypeDto {
|
||||
final int value;
|
||||
final String name;
|
||||
|
||||
VacationTypeDto({
|
||||
required this.value,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
factory VacationTypeDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationTypeDto(
|
||||
value: json['value'] ?? 0,
|
||||
name: json['name']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
54
lib/data/dto/vacations_list_response_dto.dart
Normal file
54
lib/data/dto/vacations_list_response_dto.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'vacation_response_dto.dart';
|
||||
|
||||
class VacationsListResponseDto {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final VacationsListDataDto? data;
|
||||
|
||||
VacationsListResponseDto({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory VacationsListResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationsListResponseDto(
|
||||
statusCode: json['statusCode'] ?? 0,
|
||||
isSuccess: json['isSuccess'] ?? false,
|
||||
message: json['message'],
|
||||
data: json['data'] != null ? VacationsListDataDto.fromJson(json['data']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VacationsListDataDto {
|
||||
final List<VacationDataDto> items;
|
||||
final int pageNumber;
|
||||
final int pageSize;
|
||||
final int totalCount;
|
||||
final int totalPages;
|
||||
|
||||
VacationsListDataDto({
|
||||
required this.items,
|
||||
required this.pageNumber,
|
||||
required this.pageSize,
|
||||
required this.totalCount,
|
||||
required this.totalPages,
|
||||
});
|
||||
|
||||
factory VacationsListDataDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationsListDataDto(
|
||||
items: json['items'] != null
|
||||
? (json['items'] as List)
|
||||
.map((item) => VacationDataDto.fromJson(item))
|
||||
.toList()
|
||||
: [],
|
||||
pageNumber: json['pageNumber'] ?? 1,
|
||||
pageSize: json['pageSize'] ?? 15,
|
||||
totalCount: json['totalCount'] ?? 0,
|
||||
totalPages: json['totalPages'] ?? 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/data/repositories/advance_repository_impl.dart
Normal file
109
lib/data/repositories/advance_repository_impl.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/exceptions.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../datasources/advance_remote_data_source.dart';
|
||||
import '../dto/advance_request_dto.dart';
|
||||
import '../../domain/models/advance_request_model.dart';
|
||||
import '../../domain/models/advances_list_response_model.dart';
|
||||
import '../../domain/repositories/advance_repository.dart';
|
||||
|
||||
class AdvanceRepositoryImpl implements AdvanceRepository {
|
||||
final AdvanceRemoteDataSource remoteDataSource;
|
||||
|
||||
AdvanceRepositoryImpl({required this.remoteDataSource});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, AdvanceResponseModel>> createAdvance(
|
||||
AdvanceRequestModel request,
|
||||
) async {
|
||||
try {
|
||||
final dto = AdvanceRequestDto(
|
||||
employeeId: request.employeeId,
|
||||
date: request.date,
|
||||
amount: request.amount,
|
||||
reason: request.reason,
|
||||
);
|
||||
|
||||
final responseDto = await remoteDataSource.createAdvance(dto);
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = AdvanceResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data != null
|
||||
? AdvanceDataModel(
|
||||
employeeId: responseDto.data!.employeeId,
|
||||
employeeFullName: responseDto.data!.employeeFullName,
|
||||
date: responseDto.data!.date,
|
||||
amount: responseDto.data!.amount,
|
||||
submittedBy: responseDto.data!.submittedBy,
|
||||
submittedByUser: responseDto.data!.submittedByUser,
|
||||
reason: responseDto.data!.reason,
|
||||
state: responseDto.data!.state,
|
||||
id: responseDto.data!.id,
|
||||
createdAt: responseDto.data!.createdAt,
|
||||
updatedAt: responseDto.data!.updatedAt,
|
||||
deletedAt: responseDto.data!.deletedAt,
|
||||
isDeleted: responseDto.data!.isDeleted,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('خطأ غير متوقع: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, AdvancesListResponseModel>> getAdvances() async {
|
||||
try {
|
||||
final responseDto = await remoteDataSource.getAdvances();
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = AdvancesListResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data != null
|
||||
? AdvancesListDataModel(
|
||||
items: responseDto.data!.items
|
||||
.map((dto) => AdvanceDataModel(
|
||||
employeeId: dto.employeeId,
|
||||
employeeFullName: dto.employeeFullName,
|
||||
date: dto.date,
|
||||
amount: dto.amount,
|
||||
submittedBy: dto.submittedBy,
|
||||
submittedByUser: dto.submittedByUser,
|
||||
reason: dto.reason,
|
||||
state: dto.state,
|
||||
id: dto.id,
|
||||
createdAt: dto.createdAt,
|
||||
updatedAt: dto.updatedAt,
|
||||
deletedAt: dto.deletedAt,
|
||||
isDeleted: dto.isDeleted,
|
||||
))
|
||||
.toList(),
|
||||
pageNumber: responseDto.data!.pageNumber,
|
||||
pageSize: responseDto.data!.pageSize,
|
||||
totalCount: responseDto.data!.totalCount,
|
||||
totalPages: responseDto.data!.totalPages,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('خطأ غير متوقع: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
41
lib/data/repositories/attendance_repository_impl.dart
Normal file
41
lib/data/repositories/attendance_repository_impl.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import '../../domain/models/attendance_login_request.dart';
|
||||
import '../../domain/models/attendance_logout_request.dart';
|
||||
import '../../domain/models/attendance_response_model.dart';
|
||||
import '../../domain/repositories/attendance_repository.dart';
|
||||
import '../datasources/attendance_remote_data_source.dart';
|
||||
|
||||
class AttendanceRepositoryImpl implements AttendanceRepository {
|
||||
final AttendanceRemoteDataSource remoteDataSource;
|
||||
|
||||
AttendanceRepositoryImpl({required this.remoteDataSource});
|
||||
|
||||
@override
|
||||
Future<AttendanceResponseModel> login(AttendanceLoginRequest request) async {
|
||||
final dto = await remoteDataSource.login(
|
||||
employeeId: request.employeeId,
|
||||
faceImage: request.faceImage,
|
||||
);
|
||||
|
||||
return AttendanceResponseModel(
|
||||
id: dto.id,
|
||||
employeeId: dto.employeeId,
|
||||
login: dto.login,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AttendanceResponseModel> logout(
|
||||
AttendanceLogoutRequest request,
|
||||
) async {
|
||||
final dto = await remoteDataSource.logout(
|
||||
employeeId: request.employeeId,
|
||||
faceImage: request.faceImage,
|
||||
);
|
||||
|
||||
return AttendanceResponseModel(
|
||||
id: dto.id,
|
||||
employeeId: dto.employeeId,
|
||||
logout: dto.logout,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import '../../core/error/failures.dart';
|
||||
import '../datasources/auth_remote_data_source.dart';
|
||||
import '../datasources/user_local_data_source.dart';
|
||||
import '../dto/login_dto.dart';
|
||||
import '../dto/login_response_dto.dart';
|
||||
import '../../domain/models/login_request.dart';
|
||||
import '../../domain/models/login_response_model.dart';
|
||||
import '../../domain/repositories/auth_repository.dart';
|
||||
@@ -19,7 +18,9 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, LoginResponseModel>> login(LoginRequest request) async {
|
||||
Future<Either<Failure, LoginResponseModel>> login(
|
||||
LoginRequest request,
|
||||
) async {
|
||||
try {
|
||||
final dto = LoginDto(
|
||||
phoneNumber: request.phoneNumber,
|
||||
@@ -27,30 +28,38 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
);
|
||||
|
||||
final responseDto = await remoteDataSource.login(dto);
|
||||
print("LOGIN RESPONSE DATA: ${responseDto.toJson()}"); // Debugging Log
|
||||
|
||||
// Cache the token locally
|
||||
if (responseDto.data?.token != null) {
|
||||
await localDataSource.cacheUserToken(responseDto.data!.token!);
|
||||
}
|
||||
if (responseDto.data?.employeeId != null) {
|
||||
print("AUTH_REPO: Caching EmployeeId: ${responseDto.data!.employeeId}");
|
||||
await localDataSource.cacheEmployeeId(responseDto.data!.employeeId!);
|
||||
} else {
|
||||
print("AUTH_REPO: EmployeeId is NULL in response!");
|
||||
}
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = LoginResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data != null
|
||||
? LoginDataModel(
|
||||
token: responseDto.data!.token,
|
||||
id: responseDto.data!.id,
|
||||
employeeId: responseDto.data!.employeeId,
|
||||
username: responseDto.data!.username,
|
||||
fullName: responseDto.data!.fullName,
|
||||
role: responseDto.data!.role,
|
||||
email: responseDto.data!.email,
|
||||
phoneNumber: responseDto.data!.phoneNumber,
|
||||
permissions: responseDto.data!.permissions,
|
||||
)
|
||||
: null,
|
||||
data:
|
||||
responseDto.data != null
|
||||
? LoginDataModel(
|
||||
token: responseDto.data!.token,
|
||||
id: responseDto.data!.id,
|
||||
employeeId: responseDto.data!.employeeId,
|
||||
username: responseDto.data!.username,
|
||||
fullName: responseDto.data!.fullName,
|
||||
role: responseDto.data!.role,
|
||||
email: responseDto.data!.email,
|
||||
phoneNumber: responseDto.data!.phoneNumber,
|
||||
permissions: responseDto.data!.permissions,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
|
||||
142
lib/data/repositories/vacation_repository_impl.dart
Normal file
142
lib/data/repositories/vacation_repository_impl.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/exceptions.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../datasources/vacation_remote_data_source.dart';
|
||||
import '../dto/vacation_request_dto.dart';
|
||||
import '../../domain/models/vacation_request.dart';
|
||||
import '../../domain/models/vacation_response_model.dart';
|
||||
import '../../domain/models/vacation_type_model.dart';
|
||||
import '../../domain/models/vacations_list_response_model.dart';
|
||||
import '../../domain/repositories/vacation_repository.dart';
|
||||
|
||||
class VacationRepositoryImpl implements VacationRepository {
|
||||
final VacationRemoteDataSource remoteDataSource;
|
||||
|
||||
VacationRepositoryImpl({required this.remoteDataSource});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, VacationResponseModel>> createVacation(
|
||||
VacationRequest request,
|
||||
) async {
|
||||
try {
|
||||
final dto = VacationRequestDto(
|
||||
employeeId: request.employeeId,
|
||||
startDate: request.startDate,
|
||||
endDate: request.endDate,
|
||||
reason: request.reason,
|
||||
type: request.type,
|
||||
);
|
||||
|
||||
final responseDto = await remoteDataSource.createVacation(dto);
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = VacationResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data != null
|
||||
? VacationDataModel(
|
||||
employeeId: responseDto.data!.employeeId,
|
||||
employeeFullName: responseDto.data!.employeeFullName,
|
||||
startDate: responseDto.data!.startDate,
|
||||
endDate: responseDto.data!.endDate,
|
||||
reason: responseDto.data!.reason,
|
||||
submittedBy: responseDto.data!.submittedBy,
|
||||
submittedByUser: responseDto.data!.submittedByUser,
|
||||
state: responseDto.data!.state,
|
||||
type: responseDto.data!.type,
|
||||
id: responseDto.data!.id,
|
||||
createdAt: responseDto.data!.createdAt,
|
||||
updatedAt: responseDto.data!.updatedAt,
|
||||
deletedAt: responseDto.data!.deletedAt,
|
||||
isDeleted: responseDto.data!.isDeleted,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('خطأ غير متوقع: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, VacationTypesResponseModel>> getVacationTypes() async {
|
||||
try {
|
||||
final responseDto = await remoteDataSource.getVacationTypes();
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = VacationTypesResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data
|
||||
.map((dto) => VacationTypeModel(
|
||||
value: dto.value,
|
||||
name: dto.name,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('خطأ غير متوقع: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, VacationsListResponseModel>> getVacations() async {
|
||||
try {
|
||||
final responseDto = await remoteDataSource.getVacations();
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = VacationsListResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data != null
|
||||
? VacationsListDataModel(
|
||||
items: responseDto.data!.items
|
||||
.map((dto) => VacationDataModel(
|
||||
employeeId: dto.employeeId,
|
||||
employeeFullName: dto.employeeFullName,
|
||||
startDate: dto.startDate,
|
||||
endDate: dto.endDate,
|
||||
reason: dto.reason,
|
||||
submittedBy: dto.submittedBy,
|
||||
submittedByUser: dto.submittedByUser,
|
||||
state: dto.state,
|
||||
type: dto.type,
|
||||
id: dto.id,
|
||||
createdAt: dto.createdAt,
|
||||
updatedAt: dto.updatedAt,
|
||||
deletedAt: dto.deletedAt,
|
||||
isDeleted: dto.isDeleted,
|
||||
))
|
||||
.toList(),
|
||||
pageNumber: responseDto.data!.pageNumber,
|
||||
pageSize: responseDto.data!.pageSize,
|
||||
totalCount: responseDto.data!.totalCount,
|
||||
totalPages: responseDto.data!.totalPages,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('خطأ غير متوقع: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
59
lib/domain/models/advance_request_model.dart
Normal file
59
lib/domain/models/advance_request_model.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
class AdvanceRequestModel {
|
||||
final String employeeId;
|
||||
final DateTime date;
|
||||
final double amount;
|
||||
final String reason;
|
||||
|
||||
AdvanceRequestModel({
|
||||
required this.employeeId,
|
||||
required this.date,
|
||||
required this.amount,
|
||||
required this.reason,
|
||||
});
|
||||
}
|
||||
|
||||
class AdvanceResponseModel {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final AdvanceDataModel? data;
|
||||
|
||||
AdvanceResponseModel({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
}
|
||||
|
||||
class AdvanceDataModel {
|
||||
final String employeeId;
|
||||
final String? employeeFullName;
|
||||
final DateTime date;
|
||||
final double amount;
|
||||
final String? submittedBy;
|
||||
final String? submittedByUser;
|
||||
final String reason;
|
||||
final int state;
|
||||
final String id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final bool? isDeleted;
|
||||
|
||||
AdvanceDataModel({
|
||||
required this.employeeId,
|
||||
this.employeeFullName,
|
||||
required this.date,
|
||||
required this.amount,
|
||||
this.submittedBy,
|
||||
this.submittedByUser,
|
||||
required this.reason,
|
||||
required this.state,
|
||||
required this.id,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.isDeleted,
|
||||
});
|
||||
}
|
||||
31
lib/domain/models/advances_list_response_model.dart
Normal file
31
lib/domain/models/advances_list_response_model.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'advance_request_model.dart';
|
||||
|
||||
class AdvancesListResponseModel {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final AdvancesListDataModel? data;
|
||||
|
||||
AdvancesListResponseModel({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
}
|
||||
|
||||
class AdvancesListDataModel {
|
||||
final List<AdvanceDataModel> items;
|
||||
final int pageNumber;
|
||||
final int pageSize;
|
||||
final int totalCount;
|
||||
final int totalPages;
|
||||
|
||||
AdvancesListDataModel({
|
||||
required this.items,
|
||||
required this.pageNumber,
|
||||
required this.pageSize,
|
||||
required this.totalCount,
|
||||
required this.totalPages,
|
||||
});
|
||||
}
|
||||
8
lib/domain/models/attendance_login_request.dart
Normal file
8
lib/domain/models/attendance_login_request.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
class AttendanceLoginRequest {
|
||||
final String employeeId;
|
||||
final File faceImage;
|
||||
|
||||
AttendanceLoginRequest({required this.employeeId, required this.faceImage});
|
||||
}
|
||||
8
lib/domain/models/attendance_logout_request.dart
Normal file
8
lib/domain/models/attendance_logout_request.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
class AttendanceLogoutRequest {
|
||||
final String employeeId;
|
||||
final File faceImage;
|
||||
|
||||
AttendanceLogoutRequest({required this.employeeId, required this.faceImage});
|
||||
}
|
||||
13
lib/domain/models/attendance_response_model.dart
Normal file
13
lib/domain/models/attendance_response_model.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
class AttendanceResponseModel {
|
||||
final String id;
|
||||
final String employeeId;
|
||||
final DateTime? login;
|
||||
final DateTime? logout;
|
||||
|
||||
AttendanceResponseModel({
|
||||
required this.id,
|
||||
required this.employeeId,
|
||||
this.login,
|
||||
this.logout,
|
||||
});
|
||||
}
|
||||
@@ -2,8 +2,5 @@ class LoginRequest {
|
||||
final String phoneNumber;
|
||||
final String password;
|
||||
|
||||
LoginRequest({
|
||||
required this.phoneNumber,
|
||||
required this.password,
|
||||
});
|
||||
LoginRequest({required this.phoneNumber, required this.password});
|
||||
}
|
||||
|
||||
15
lib/domain/models/vacation_request.dart
Normal file
15
lib/domain/models/vacation_request.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
class VacationRequest {
|
||||
final String employeeId;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final String reason;
|
||||
final int type;
|
||||
|
||||
VacationRequest({
|
||||
required this.employeeId,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.reason,
|
||||
required this.type,
|
||||
});
|
||||
}
|
||||
47
lib/domain/models/vacation_response_model.dart
Normal file
47
lib/domain/models/vacation_response_model.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
class VacationResponseModel {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String message;
|
||||
final VacationDataModel? data;
|
||||
|
||||
VacationResponseModel({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
required this.message,
|
||||
this.data,
|
||||
});
|
||||
}
|
||||
|
||||
class VacationDataModel {
|
||||
final String employeeId;
|
||||
final String? employeeFullName;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final String reason;
|
||||
final String? submittedBy;
|
||||
final String? submittedByUser;
|
||||
final int state;
|
||||
final int type;
|
||||
final String id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final bool? isDeleted;
|
||||
|
||||
VacationDataModel({
|
||||
required this.employeeId,
|
||||
this.employeeFullName,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.reason,
|
||||
this.submittedBy,
|
||||
this.submittedByUser,
|
||||
required this.state,
|
||||
required this.type,
|
||||
required this.id,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.isDeleted,
|
||||
});
|
||||
}
|
||||
23
lib/domain/models/vacation_type_model.dart
Normal file
23
lib/domain/models/vacation_type_model.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
class VacationTypeModel {
|
||||
final int value;
|
||||
final String name;
|
||||
|
||||
VacationTypeModel({
|
||||
required this.value,
|
||||
required this.name,
|
||||
});
|
||||
}
|
||||
|
||||
class VacationTypesResponseModel {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final List<VacationTypeModel> data;
|
||||
|
||||
VacationTypesResponseModel({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
required this.data,
|
||||
});
|
||||
}
|
||||
31
lib/domain/models/vacations_list_response_model.dart
Normal file
31
lib/domain/models/vacations_list_response_model.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'vacation_response_model.dart';
|
||||
|
||||
class VacationsListResponseModel {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final VacationsListDataModel? data;
|
||||
|
||||
VacationsListResponseModel({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
}
|
||||
|
||||
class VacationsListDataModel {
|
||||
final List<VacationDataModel> items;
|
||||
final int pageNumber;
|
||||
final int pageSize;
|
||||
final int totalCount;
|
||||
final int totalPages;
|
||||
|
||||
VacationsListDataModel({
|
||||
required this.items,
|
||||
required this.pageNumber,
|
||||
required this.pageSize,
|
||||
required this.totalCount,
|
||||
required this.totalPages,
|
||||
});
|
||||
}
|
||||
11
lib/domain/repositories/advance_repository.dart
Normal file
11
lib/domain/repositories/advance_repository.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/advance_request_model.dart';
|
||||
import '../models/advances_list_response_model.dart';
|
||||
|
||||
abstract class AdvanceRepository {
|
||||
Future<Either<Failure, AdvanceResponseModel>> createAdvance(
|
||||
AdvanceRequestModel request,
|
||||
);
|
||||
Future<Either<Failure, AdvancesListResponseModel>> getAdvances();
|
||||
}
|
||||
12
lib/domain/repositories/attendance_repository.dart
Normal file
12
lib/domain/repositories/attendance_repository.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import '../models/attendance_login_request.dart';
|
||||
import '../models/attendance_logout_request.dart';
|
||||
import '../models/attendance_response_model.dart';
|
||||
|
||||
//in the following polymorphism is being used , a quich recap it is where th esame method but opperate in a different way
|
||||
|
||||
//one Repo two requests
|
||||
abstract class AttendanceRepository {
|
||||
Future<AttendanceResponseModel> login(AttendanceLoginRequest request);
|
||||
|
||||
Future<AttendanceResponseModel> logout(AttendanceLogoutRequest request);
|
||||
}
|
||||
14
lib/domain/repositories/vacation_repository.dart
Normal file
14
lib/domain/repositories/vacation_repository.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/vacation_request.dart';
|
||||
import '../models/vacation_response_model.dart';
|
||||
import '../models/vacation_type_model.dart';
|
||||
import '../models/vacations_list_response_model.dart';
|
||||
|
||||
abstract class VacationRepository {
|
||||
Future<Either<Failure, VacationResponseModel>> createVacation(
|
||||
VacationRequest request,
|
||||
);
|
||||
Future<Either<Failure, VacationTypesResponseModel>> getVacationTypes();
|
||||
Future<Either<Failure, VacationsListResponseModel>> getVacations();
|
||||
}
|
||||
15
lib/domain/usecases/attendance_login_usecase.dart
Normal file
15
lib/domain/usecases/attendance_login_usecase.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import '../models/attendance_login_request.dart';
|
||||
import '../models/attendance_response_model.dart';
|
||||
import '../repositories/attendance_repository.dart';
|
||||
|
||||
//always remmber that the usecase uses the repo
|
||||
|
||||
class AttendanceLoginUsecase {
|
||||
final AttendanceRepository repository;
|
||||
|
||||
AttendanceLoginUsecase({required this.repository});
|
||||
|
||||
Future<AttendanceResponseModel> call(AttendanceLoginRequest request) {
|
||||
return repository.login(request);
|
||||
}
|
||||
}
|
||||
13
lib/domain/usecases/attendance_logout_usecase.dart
Normal file
13
lib/domain/usecases/attendance_logout_usecase.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import '../models/attendance_logout_request.dart';
|
||||
import '../models/attendance_response_model.dart';
|
||||
import '../repositories/attendance_repository.dart';
|
||||
|
||||
class AttendanceLogoutUseCase {
|
||||
final AttendanceRepository repository;
|
||||
|
||||
AttendanceLogoutUseCase({required this.repository});
|
||||
|
||||
Future<AttendanceResponseModel> call(AttendanceLogoutRequest request) {
|
||||
return repository.logout(request);
|
||||
}
|
||||
}
|
||||
14
lib/domain/usecases/create_advance_usecase.dart
Normal file
14
lib/domain/usecases/create_advance_usecase.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/advance_request_model.dart';
|
||||
import '../repositories/advance_repository.dart';
|
||||
|
||||
class CreateAdvanceUseCase {
|
||||
final AdvanceRepository repository;
|
||||
|
||||
CreateAdvanceUseCase({required this.repository});
|
||||
|
||||
Future<Either<Failure, AdvanceResponseModel>> call(AdvanceRequestModel request) {
|
||||
return repository.createAdvance(request);
|
||||
}
|
||||
}
|
||||
15
lib/domain/usecases/create_vacation_usecase.dart
Normal file
15
lib/domain/usecases/create_vacation_usecase.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/vacation_request.dart';
|
||||
import '../models/vacation_response_model.dart';
|
||||
import '../repositories/vacation_repository.dart';
|
||||
|
||||
class CreateVacationUseCase {
|
||||
final VacationRepository repository;
|
||||
|
||||
CreateVacationUseCase({required this.repository});
|
||||
|
||||
Future<Either<Failure, VacationResponseModel>> call(VacationRequest request) {
|
||||
return repository.createVacation(request);
|
||||
}
|
||||
}
|
||||
14
lib/domain/usecases/get_advances_usecase.dart
Normal file
14
lib/domain/usecases/get_advances_usecase.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/advances_list_response_model.dart';
|
||||
import '../repositories/advance_repository.dart';
|
||||
|
||||
class GetAdvancesUseCase {
|
||||
final AdvanceRepository repository;
|
||||
|
||||
GetAdvancesUseCase({required this.repository});
|
||||
|
||||
Future<Either<Failure, AdvancesListResponseModel>> call() {
|
||||
return repository.getAdvances();
|
||||
}
|
||||
}
|
||||
14
lib/domain/usecases/get_vacation_types_usecase.dart
Normal file
14
lib/domain/usecases/get_vacation_types_usecase.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/vacation_type_model.dart';
|
||||
import '../repositories/vacation_repository.dart';
|
||||
|
||||
class GetVacationTypesUseCase {
|
||||
final VacationRepository repository;
|
||||
|
||||
GetVacationTypesUseCase({required this.repository});
|
||||
|
||||
Future<Either<Failure, VacationTypesResponseModel>> call() {
|
||||
return repository.getVacationTypes();
|
||||
}
|
||||
}
|
||||
14
lib/domain/usecases/get_vacations_usecase.dart
Normal file
14
lib/domain/usecases/get_vacations_usecase.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/vacations_list_response_model.dart';
|
||||
import '../repositories/vacation_repository.dart';
|
||||
|
||||
class GetVacationsUseCase {
|
||||
final VacationRepository repository;
|
||||
|
||||
GetVacationsUseCase({required this.repository});
|
||||
|
||||
Future<Either<Failure, VacationsListResponseModel>> call() {
|
||||
return repository.getVacations();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
|
||||
import 'core/di/injection_container.dart';
|
||||
import 'screens/splash_screen.dart';
|
||||
import 'presentation/screens/splash_screen.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
40
lib/presentation/blocs/login/login_bloc.dart
Normal file
40
lib/presentation/blocs/login/login_bloc.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/usecases/login_usecase.dart';
|
||||
import 'login_event.dart';
|
||||
import 'login_state.dart';
|
||||
|
||||
class LoginBloc extends Bloc<LoginEvent, LoginState> {
|
||||
final LoginUseCase loginUseCase;
|
||||
|
||||
LoginBloc({required this.loginUseCase}) : super(const LoginInitial()) {
|
||||
on<LoginSubmitted>(_onLoginSubmitted);
|
||||
on<LoginReset>(_onLoginReset);
|
||||
}
|
||||
|
||||
Future<void> _onLoginSubmitted(
|
||||
LoginSubmitted event,
|
||||
Emitter<LoginState> emit,
|
||||
) async {
|
||||
emit(const LoginLoading());
|
||||
|
||||
final result = await loginUseCase(event.request);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(LoginError(failure.message)),
|
||||
(response) {
|
||||
if (response.isSuccess) {
|
||||
emit(LoginSuccess(response));
|
||||
} else {
|
||||
emit(LoginError(response.message));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onLoginReset(
|
||||
LoginReset event,
|
||||
Emitter<LoginState> emit,
|
||||
) {
|
||||
emit(const LoginInitial());
|
||||
}
|
||||
}
|
||||
22
lib/presentation/blocs/login/login_event.dart
Normal file
22
lib/presentation/blocs/login/login_event.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../domain/models/login_request.dart';
|
||||
|
||||
abstract class LoginEvent extends Equatable {
|
||||
const LoginEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoginSubmitted extends LoginEvent {
|
||||
final LoginRequest request;
|
||||
|
||||
const LoginSubmitted(this.request);
|
||||
|
||||
@override
|
||||
List<Object> get props => [request];
|
||||
}
|
||||
|
||||
class LoginReset extends LoginEvent {
|
||||
const LoginReset();
|
||||
}
|
||||
35
lib/presentation/blocs/login/login_state.dart
Normal file
35
lib/presentation/blocs/login/login_state.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../domain/models/login_response_model.dart';
|
||||
|
||||
abstract class LoginState extends Equatable {
|
||||
const LoginState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoginInitial extends LoginState {
|
||||
const LoginInitial();
|
||||
}
|
||||
|
||||
class LoginLoading extends LoginState {
|
||||
const LoginLoading();
|
||||
}
|
||||
|
||||
class LoginSuccess extends LoginState {
|
||||
final LoginResponseModel response;
|
||||
|
||||
const LoginSuccess(this.response);
|
||||
|
||||
@override
|
||||
List<Object> get props => [response];
|
||||
}
|
||||
|
||||
class LoginError extends LoginState {
|
||||
final String message;
|
||||
|
||||
const LoginError(this.message);
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
import 'package:coda_project/screens/face_screen.dart';
|
||||
import 'package:coda_project/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/screens/user_settings_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/face_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../domain/models/attendance_login_request.dart';
|
||||
import '../../domain/models/attendance_logout_request.dart';
|
||||
import '../../domain/usecases/attendance_login_usecase.dart';
|
||||
import '../../domain/usecases/attendance_logout_usecase.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
|
||||
class AttendanceScreen extends StatelessWidget {
|
||||
const AttendanceScreen({super.key});
|
||||
@@ -138,12 +144,40 @@ class AttendanceScreen extends StatelessWidget {
|
||||
child: _FingerButton(
|
||||
icon: "assets/images/faceLogin.svg",
|
||||
label: "تسجيل الدخول",
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => OvalCameraCapturePage(isLogin: true),
|
||||
),
|
||||
);
|
||||
onTap: () async {
|
||||
final employeeId =
|
||||
await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||
print("ATTENDANCE_SCREEN: Retrieved EmployeeId: $employeeId");
|
||||
if (employeeId == null) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(_) => OvalCameraCapturePage(
|
||||
isLogin: true,
|
||||
onCapture: (imageFile) async {
|
||||
final loginUseCase =
|
||||
sl<AttendanceLoginUsecase>();
|
||||
await loginUseCase(
|
||||
AttendanceLoginRequest(
|
||||
employeeId: employeeId,
|
||||
faceImage: imageFile,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -178,12 +212,39 @@ class AttendanceScreen extends StatelessWidget {
|
||||
child: _FingerButton(
|
||||
icon: "assets/images/faceLogout.svg",
|
||||
label: "تسجيل خروج",
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => OvalCameraCapturePage(isLogin: false),
|
||||
),
|
||||
);
|
||||
onTap: () async {
|
||||
final employeeId =
|
||||
await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||
if (employeeId == null) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(_) => OvalCameraCapturePage(
|
||||
isLogin: false,
|
||||
onCapture: (imageFile) async {
|
||||
final logoutUseCase =
|
||||
sl<AttendanceLogoutUseCase>();
|
||||
await logoutUseCase(
|
||||
AttendanceLogoutRequest(
|
||||
employeeId: employeeId,
|
||||
faceImage: imageFile,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
34
lib/presentation/screens/auth_screen.dart
Normal file
34
lib/presentation/screens/auth_screen.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/auth_form.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../blocs/login/login_bloc.dart';
|
||||
|
||||
class AuthScreen extends StatelessWidget {
|
||||
const AuthScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => sl<LoginBloc>(),
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: AppBackground(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
// Logo
|
||||
Center(child: Image.asset("assets/images/logo2.png", width: 200)),
|
||||
// const SizedBox(height: 15),
|
||||
// Form - taking remaining space and centered
|
||||
Expanded(child: Center(child: const AuthForm())),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,18 @@ import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import '../../core/error/exceptions.dart';
|
||||
|
||||
class OvalCameraCapturePage extends StatefulWidget {
|
||||
final bool isLogin;
|
||||
const OvalCameraCapturePage({super.key, this.isLogin = true});
|
||||
final Future<void> Function(File image) onCapture;
|
||||
|
||||
const OvalCameraCapturePage({
|
||||
super.key,
|
||||
this.isLogin = true,
|
||||
required this.onCapture,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OvalCameraCapturePage> createState() => _OvalCameraCapturePageState();
|
||||
@@ -16,6 +24,7 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
bool _isCameraInitialized = false;
|
||||
String? _errorMessage;
|
||||
bool _isSuccess = false;
|
||||
bool _isLoading = false;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
@@ -26,6 +35,11 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
|
||||
Future<void> _initializeCamera() async {
|
||||
try {
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
|
||||
// Dispose existing controller if any
|
||||
await _cameraController?.dispose();
|
||||
_cameraController = null;
|
||||
@@ -35,8 +49,9 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
|
||||
// Check if cameras list is available
|
||||
if (cameras.isEmpty) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_errorMessage = "لا توجد كاميرات متاحة";
|
||||
_errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
return;
|
||||
@@ -53,8 +68,9 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
if (cameras.isNotEmpty) {
|
||||
selectedCamera = cameras.first;
|
||||
} else {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_errorMessage = "لا توجد كاميرات متاحة";
|
||||
_errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
return;
|
||||
@@ -77,31 +93,143 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
_timer = Timer(const Duration(seconds: 3), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSuccess = true;
|
||||
});
|
||||
|
||||
// Auto-close after 2 seconds
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
_startScan();
|
||||
} on CameraException catch (e) {
|
||||
if (!mounted) return;
|
||||
String errorMessage;
|
||||
switch (e.code) {
|
||||
case 'CameraAccessDenied':
|
||||
errorMessage = "تم رفض الوصول إلى الكاميرا. يرجى السماح بالوصول في الإعدادات";
|
||||
break;
|
||||
case 'CameraAccessDeniedWithoutPrompt':
|
||||
errorMessage = "تم رفض الوصول إلى الكاميرا. يرجى السماح بالوصول في الإعدادات";
|
||||
break;
|
||||
case 'CameraAccessRestricted':
|
||||
errorMessage = "الوصول إلى الكاميرا مقيد";
|
||||
break;
|
||||
case 'AudioAccessDenied':
|
||||
errorMessage = "تم رفض الوصول إلى الميكروفون";
|
||||
break;
|
||||
default:
|
||||
errorMessage = "خطأ في تهيئة الكاميرا: ${e.description ?? 'خطأ غير معروف'}";
|
||||
}
|
||||
setState(() {
|
||||
_errorMessage = errorMessage;
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_errorMessage = "خطأ في تهيئة الكاميرا: $e";
|
||||
_errorMessage = "حدث خطأ غير متوقع أثناء تهيئة الكاميرا";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
print("Error initializing camera: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startScan() async {
|
||||
try {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
// Simulate scanning delay
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
if (!mounted ||
|
||||
_cameraController == null ||
|
||||
!_cameraController!.value.isInitialized) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = "الكاميرا غير مهيأة. يرجى المحاولة مرة أخرى";
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final xFile = await _cameraController!.takePicture();
|
||||
final file = File(xFile.path);
|
||||
|
||||
// Check if file exists and is readable
|
||||
if (!await file.exists()) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = "فشل حفظ الصورة. يرجى المحاولة مرة أخرى";
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the onCapture callback which may throw exceptions
|
||||
await widget.onCapture(file);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSuccess = true;
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
// Auto-close after 2 seconds
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
} on ServerException catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = e.message;
|
||||
});
|
||||
}
|
||||
} on NetworkException catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = e.message;
|
||||
});
|
||||
}
|
||||
} on CameraException catch (e) {
|
||||
if (mounted) {
|
||||
String errorMessage;
|
||||
switch (e.code) {
|
||||
case 'captureAlreadyActive':
|
||||
errorMessage = "جاري التقاط صورة بالفعل. يرجى الانتظار";
|
||||
break;
|
||||
case 'pictureTakingInProgress':
|
||||
errorMessage = "جاري التقاط صورة. يرجى الانتظار";
|
||||
break;
|
||||
default:
|
||||
errorMessage = "فشل التقاط الصورة: ${e.description ?? 'خطأ غير معروف'}";
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = errorMessage;
|
||||
});
|
||||
}
|
||||
} on FileSystemException catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = "فشل حفظ الصورة. يرجى التحقق من مساحة التخزين";
|
||||
});
|
||||
}
|
||||
print("File system error: $e");
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى";
|
||||
});
|
||||
}
|
||||
print("Unexpected error in _startScan: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cameraController?.dispose();
|
||||
@@ -115,7 +243,8 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
backgroundColor: Color(0xff000000),
|
||||
|
||||
body:
|
||||
_errorMessage != null
|
||||
// Show error screen only if camera failed to initialize
|
||||
_errorMessage != null && !_isCameraInitialized
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
@@ -138,17 +267,39 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _initializeCamera,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xffE8001A),
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: _initializeCamera,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xffE8001A),
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
child: Text("إعادة المحاولة"),
|
||||
),
|
||||
),
|
||||
child: Text("إعادة المحاولة"),
|
||||
SizedBox(width: 16),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(false);
|
||||
}
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: BorderSide(color: Colors.white70),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
child: Text("إلغاء"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -200,9 +351,13 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
? (widget.isLogin
|
||||
? "تم تسجيل دخولك بنجاح"
|
||||
: "تم تسجيل خروجك بنجاح")
|
||||
: (widget.isLogin
|
||||
? "يتم تسجيل الدخول ..."
|
||||
: "يتم تسجيل الخروج ..."),
|
||||
: _isLoading
|
||||
? (widget.isLogin
|
||||
? "يتم تسجيل الدخول ..."
|
||||
: "يتم تسجيل الخروج ...")
|
||||
: (widget.isLogin
|
||||
? "جاهز للتقاط الصورة"
|
||||
: "جاهز للتقاط الصورة"),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
@@ -227,6 +382,81 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
),
|
||||
),
|
||||
|
||||
// Error overlay (shown when error occurs during scanning)
|
||||
if (_errorMessage != null && _isCameraInitialized)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.red,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
_isLoading = false;
|
||||
});
|
||||
_startScan();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xffE8001A),
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
child: Text("إعادة المحاولة"),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(false);
|
||||
}
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: BorderSide(color: Colors.white70),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
child: Text("إلغاء"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// // Capture button
|
||||
// Positioned(
|
||||
// bottom: 60,
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:coda_project/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/screens/user_settings_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/finance_summary_card.dart';
|
||||
import '../widgets/work_day_card.dart';
|
||||
@@ -1,15 +1,23 @@
|
||||
import 'package:coda_project/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/screens/user_settings_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../screens/request_leave_screen.dart';
|
||||
import '../screens/request_advance_scrren.dart';
|
||||
import '../models/leave_request.dart';
|
||||
import '../models/advance_request.dart';
|
||||
import '../services/request_service.dart';
|
||||
import 'request_leave_screen.dart';
|
||||
import 'request_advance_scrren.dart';
|
||||
import '../../models/leave_request.dart';
|
||||
import '../../models/advance_request.dart';
|
||||
import '../../core/services/request_service.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../domain/usecases/get_vacations_usecase.dart';
|
||||
import '../../domain/usecases/get_advances_usecase.dart';
|
||||
import '../../domain/models/vacations_list_response_model.dart';
|
||||
import '../../domain/models/vacation_response_model.dart';
|
||||
import '../../domain/models/advances_list_response_model.dart';
|
||||
import '../../domain/models/advance_request_model.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
|
||||
class HolidayScreen extends StatefulWidget {
|
||||
final void Function(bool isScrollingDown)? onScrollEvent;
|
||||
@@ -24,8 +32,12 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
||||
int activeTab = 0;
|
||||
|
||||
final RequestService _requestService = RequestService();
|
||||
final GetVacationsUseCase _getVacationsUseCase = sl<GetVacationsUseCase>();
|
||||
final GetAdvancesUseCase _getAdvancesUseCase = sl<GetAdvancesUseCase>();
|
||||
List<LeaveRequest> _leaveRequests = [];
|
||||
List<AdvanceRequest> _advanceRequests = [];
|
||||
bool _isLoadingVacations = false;
|
||||
bool _isLoadingAdvances = false;
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _showActions = true;
|
||||
@@ -62,11 +74,18 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
||||
}
|
||||
|
||||
void _initializeData() async {
|
||||
_leaveRequests = await _requestService.getLeaveRequests();
|
||||
_advanceRequests = await _requestService.getAdvanceRequests();
|
||||
// Load from API
|
||||
_loadVacationsFromAPI();
|
||||
_loadAdvancesFromAPI();
|
||||
|
||||
// Also listen to local changes (for newly created requests)
|
||||
_requestService.leaveRequestsStream.listen((requests) {
|
||||
if (mounted) setState(() => _leaveRequests = requests);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
// Merge with API data if needed
|
||||
_leaveRequests = requests;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_requestService.advanceRequestsStream.listen((requests) {
|
||||
@@ -74,6 +93,140 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadVacationsFromAPI() async {
|
||||
setState(() {
|
||||
_isLoadingVacations = true;
|
||||
});
|
||||
|
||||
final result = await _getVacationsUseCase();
|
||||
result.fold(
|
||||
(failure) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingVacations = false;
|
||||
});
|
||||
// Load from local service as fallback
|
||||
_requestService.getLeaveRequests().then((requests) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_leaveRequests = requests;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
(response) {
|
||||
if (mounted && response.data != null) {
|
||||
setState(() {
|
||||
_leaveRequests = response.data!.items
|
||||
.map((vacation) => _convertVacationToLeaveRequest(vacation))
|
||||
.toList();
|
||||
_isLoadingVacations = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
LeaveRequest _convertVacationToLeaveRequest(VacationDataModel vacation) {
|
||||
// Convert state (0=waiting, 1=approved, 2=denied) to status string
|
||||
String status = "waiting";
|
||||
if (vacation.state == 1) {
|
||||
status = "approved";
|
||||
} else if (vacation.state == 2) {
|
||||
status = "denied";
|
||||
}
|
||||
|
||||
// Convert type to Arabic name
|
||||
String leaveTypeName = _getArabicVacationTypeName(vacation.type);
|
||||
|
||||
// Check if it's timed leave (same day but different times)
|
||||
bool isTimedLeave = vacation.startDate.year == vacation.endDate.year &&
|
||||
vacation.startDate.month == vacation.endDate.month &&
|
||||
vacation.startDate.day == vacation.endDate.day &&
|
||||
vacation.startDate.hour != vacation.endDate.hour;
|
||||
|
||||
return LeaveRequest(
|
||||
id: vacation.id,
|
||||
leaveType: leaveTypeName,
|
||||
isTimedLeave: isTimedLeave,
|
||||
fromDate: vacation.startDate,
|
||||
toDate: vacation.endDate,
|
||||
fromTime: TimeOfDay.fromDateTime(vacation.startDate),
|
||||
toTime: TimeOfDay.fromDateTime(vacation.endDate),
|
||||
reason: vacation.reason,
|
||||
requestDate: vacation.createdAt ?? vacation.startDate,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
|
||||
String _getArabicVacationTypeName(int type) {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return 'أجازة زمنية';
|
||||
case 2:
|
||||
return 'إجازة مرضية';
|
||||
case 3:
|
||||
return 'إجازة مدفوعة';
|
||||
case 4:
|
||||
return 'إجازة غير مدفوعة';
|
||||
default:
|
||||
return 'إجازة';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadAdvancesFromAPI() async {
|
||||
setState(() {
|
||||
_isLoadingAdvances = true;
|
||||
});
|
||||
|
||||
final result = await _getAdvancesUseCase();
|
||||
result.fold(
|
||||
(failure) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingAdvances = false;
|
||||
});
|
||||
// Load from local service as fallback
|
||||
_requestService.getAdvanceRequests().then((requests) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_advanceRequests = requests;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
(response) {
|
||||
if (mounted && response.data != null) {
|
||||
setState(() {
|
||||
_advanceRequests = response.data!.items
|
||||
.map((advance) => _convertAdvanceToAdvanceRequest(advance))
|
||||
.toList();
|
||||
_isLoadingAdvances = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
AdvanceRequest _convertAdvanceToAdvanceRequest(AdvanceDataModel advance) {
|
||||
// Convert state (0=waiting, 1=approved, 2=denied) to status string
|
||||
String status = "waiting";
|
||||
if (advance.state == 1) {
|
||||
status = "approved";
|
||||
} else if (advance.state == 2) {
|
||||
status = "denied";
|
||||
}
|
||||
|
||||
return AdvanceRequest(
|
||||
id: advance.id,
|
||||
amount: advance.amount,
|
||||
reason: advance.reason,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
@@ -259,6 +412,19 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
Widget _buildLeaveRequestsSliver() {
|
||||
if (_isLoadingVacations) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF8EFDC2),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_leaveRequests.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
@@ -287,6 +453,19 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
||||
}
|
||||
|
||||
Widget _buildAdvanceRequestsSliver() {
|
||||
if (_isLoadingAdvances) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF8EFDC2),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_advanceRequests.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/floatingnavbar.dart';
|
||||
import '../screens/attendence_screen.dart';
|
||||
import '../screens/finance_screen.dart';
|
||||
import '../screens/holiday_screen.dart';
|
||||
import '../widgets/FloatingNavBar.dart';
|
||||
import 'attendence_screen.dart';
|
||||
import 'finance_screen.dart';
|
||||
import 'holiday_screen.dart';
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
const MainPage({super.key});
|
||||
@@ -15,20 +15,17 @@ class MainPage extends StatefulWidget {
|
||||
class _MainPageState extends State<MainPage> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.sizeOf(context).height;
|
||||
// final screenHeight = MediaQuery.sizeOf(context).height;
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
|
||||
/// BACKGROUND
|
||||
const AppBackground(child: SizedBox()),
|
||||
|
||||
/// ACTIVE SCREEN (fills entire screen - content will extend behind navbar)
|
||||
///
|
||||
///
|
||||
Positioned.fill(
|
||||
child: IndexedStack(
|
||||
index: _currentIndex,
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:coda_project/screens/auth_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/auth_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/onboarding_page.dart';
|
||||
import '../widgets/onboarding_button.dart';
|
||||
@@ -3,8 +3,13 @@ import 'package:flutter/material.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../widgets/onboarding_button.dart';
|
||||
import '../models/advance_request.dart';
|
||||
import '../services/request_service.dart';
|
||||
import '../../models/advance_request.dart';
|
||||
import '../../core/services/request_service.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
import '../../domain/usecases/create_advance_usecase.dart';
|
||||
import '../../domain/models/advance_request_model.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
|
||||
class RequestAdvanceScreen extends StatefulWidget {
|
||||
const RequestAdvanceScreen({super.key});
|
||||
@@ -23,16 +28,30 @@ class _RequestAdvanceScreenState extends State<RequestAdvanceScreen> {
|
||||
// Use the singleton instance
|
||||
final RequestService _requestService = RequestService();
|
||||
|
||||
// Use case
|
||||
final CreateAdvanceUseCase _createAdvanceUseCase = sl<CreateAdvanceUseCase>();
|
||||
|
||||
String _getFailureMessage(Failure failure) {
|
||||
if (failure is ServerFailure) {
|
||||
return failure.message;
|
||||
} else if (failure is NetworkFailure) {
|
||||
return failure.message;
|
||||
}
|
||||
return 'حدث خطأ غير متوقع';
|
||||
}
|
||||
|
||||
// Method to save the advance request
|
||||
Future<void> _saveAdvanceRequest() async {
|
||||
if (amountController.text.isEmpty || reasonController.text.isEmpty) {
|
||||
// Show an error message if fields are empty
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال جميع الحقول'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال جميع الحقول'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,45 +59,96 @@ class _RequestAdvanceScreenState extends State<RequestAdvanceScreen> {
|
||||
final amount = double.tryParse(amountController.text);
|
||||
if (amount == null || amount <= 0) {
|
||||
// Show an error message if amount is invalid
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال مبلغ صحيح'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال مبلغ صحيح'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new advance request with default status "waiting"
|
||||
final advanceRequest = AdvanceRequest(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
amount: amount,
|
||||
reason: reasonController.text,
|
||||
status: "waiting", // Default status
|
||||
);
|
||||
// Get employee ID
|
||||
final employeeId = await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||
if (employeeId == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading indicator
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Save the advance request
|
||||
await _requestService.addAdvanceRequest(advanceRequest);
|
||||
|
||||
// Show a success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم إرسال طلب السلفة بنجاح'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
// Create advance request model
|
||||
final advanceRequestModel = AdvanceRequestModel(
|
||||
employeeId: employeeId,
|
||||
date: DateTime.now(),
|
||||
amount: amount,
|
||||
reason: reasonController.text,
|
||||
);
|
||||
|
||||
// Navigate back to the previous screen
|
||||
Navigator.pop(context);
|
||||
final result = await _createAdvanceUseCase(advanceRequestModel);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // Close loading dialog
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_getFailureMessage(failure)),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
},
|
||||
(response) {
|
||||
// Also save locally for UI display
|
||||
final advanceRequest = AdvanceRequest(
|
||||
id: response.data?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
amount: amount,
|
||||
reason: reasonController.text,
|
||||
status: "waiting", // Default status
|
||||
);
|
||||
_requestService.addAdvanceRequest(advanceRequest);
|
||||
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم إرسال طلب السلفة بنجاح'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate back to the previous screen
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Show an error message if something went wrong
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('حدث خطأ: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // Close loading dialog
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('حدث خطأ غير متوقع: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,15 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../widgets/onboarding_button.dart';
|
||||
import '../models/leave_request.dart';
|
||||
import '../services/request_service.dart';
|
||||
import '../../models/leave_request.dart';
|
||||
import '../../core/services/request_service.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
import '../../domain/usecases/create_vacation_usecase.dart';
|
||||
import '../../domain/usecases/get_vacation_types_usecase.dart';
|
||||
import '../../domain/models/vacation_request.dart';
|
||||
import '../../domain/models/vacation_type_model.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
|
||||
class RequestLeaveScreen extends StatefulWidget {
|
||||
const RequestLeaveScreen({super.key});
|
||||
@@ -33,6 +40,14 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
|
||||
// Use the singleton instance
|
||||
final RequestService _requestService = RequestService();
|
||||
|
||||
// Use cases
|
||||
final CreateVacationUseCase _createVacationUseCase = sl<CreateVacationUseCase>();
|
||||
final GetVacationTypesUseCase _getVacationTypesUseCase = sl<GetVacationTypesUseCase>();
|
||||
|
||||
// Vacation types from API
|
||||
List<VacationTypeModel> _vacationTypes = [];
|
||||
int? _selectedVacationTypeValue; // Store selected type value instead of string
|
||||
|
||||
/// PICK DATE
|
||||
Future<void> pickDate(bool isFrom) async {
|
||||
DateTime initial = isFrom ? fromDate! : toDate!;
|
||||
@@ -79,52 +94,211 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadVacationTypes();
|
||||
}
|
||||
|
||||
Future<void> _loadVacationTypes() async {
|
||||
final result = await _getVacationTypesUseCase();
|
||||
result.fold(
|
||||
(failure) {
|
||||
// Silently fail - user can still submit with default types
|
||||
print('Failed to load vacation types: ${_getFailureMessage(failure)}');
|
||||
},
|
||||
(response) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_vacationTypes = response.data;
|
||||
// Set default to SickLeave (value: 2) if available
|
||||
if (_vacationTypes.isNotEmpty) {
|
||||
final sickLeave = _vacationTypes.firstWhere(
|
||||
(type) => type.value == 2,
|
||||
orElse: () => _vacationTypes.first,
|
||||
);
|
||||
_selectedVacationTypeValue = sickLeave.value;
|
||||
leaveType = _getArabicName(sickLeave.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _getArabicName(String apiName) {
|
||||
// Map API names to Arabic display names
|
||||
switch (apiName) {
|
||||
case 'TimeOff':
|
||||
return 'أجازة زمنية';
|
||||
case 'SickLeave':
|
||||
return 'إجازة مرضية';
|
||||
case 'PaidLeave':
|
||||
return 'إجازة مدفوعة';
|
||||
case 'UnpaidLeave':
|
||||
return 'إجازة غير مدفوعة';
|
||||
default:
|
||||
return apiName;
|
||||
}
|
||||
}
|
||||
|
||||
int _getVacationTypeValue() {
|
||||
// Use selected value if available, otherwise fallback to mapping
|
||||
if (_selectedVacationTypeValue != null) {
|
||||
return _selectedVacationTypeValue!;
|
||||
}
|
||||
// Fallback: Map display names to API type values
|
||||
if (leaveType.contains("مرضية") || leaveType == "SickLeave") {
|
||||
return 2; // SickLeave
|
||||
} else if (leaveType.contains("مدفوعة") && !leaveType.contains("غير")) {
|
||||
return 3; // PaidLeave
|
||||
} else if (leaveType.contains("غير مدفوعة") || leaveType == "UnpaidLeave") {
|
||||
return 4; // UnpaidLeave
|
||||
} else if (leaveType.contains("زمنية") || leaveType == "TimeOff") {
|
||||
return 1; // TimeOff
|
||||
}
|
||||
return 1; // Default to TimeOff
|
||||
}
|
||||
|
||||
String _getFailureMessage(Failure failure) {
|
||||
if (failure is ServerFailure) {
|
||||
return failure.message;
|
||||
} else if (failure is NetworkFailure) {
|
||||
return failure.message;
|
||||
}
|
||||
return 'حدث خطأ غير متوقع';
|
||||
}
|
||||
|
||||
// Method to save the leave request
|
||||
Future<void> _saveLeaveRequest() async {
|
||||
if (reasonController.text.isEmpty) {
|
||||
// Show an error message if reason is empty
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال السبب'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال السبب'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new leave request with default status "waiting"
|
||||
final leaveRequest = LeaveRequest(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
leaveType: leaveType, // Use the current leaveType value
|
||||
isTimedLeave: isTimedLeave,
|
||||
fromDate: fromDate!,
|
||||
toDate: toDate!,
|
||||
fromTime: fromTime!,
|
||||
toTime: toTime!,
|
||||
reason: reasonController.text,
|
||||
requestDate: DateTime.now(),
|
||||
status: "waiting", // Default status
|
||||
);
|
||||
// Get employee ID
|
||||
final employeeId = await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||
if (employeeId == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare dates - if timed leave, use same date with time differences
|
||||
DateTime finalStartDate = fromDate!;
|
||||
DateTime finalEndDate = toDate!;
|
||||
|
||||
if (isTimedLeave) {
|
||||
// For timed leave, use the same date but with different times
|
||||
finalStartDate = DateTime(
|
||||
fromDate!.year,
|
||||
fromDate!.month,
|
||||
fromDate!.day,
|
||||
fromTime!.hour,
|
||||
fromTime!.minute,
|
||||
);
|
||||
finalEndDate = DateTime(
|
||||
fromDate!.year,
|
||||
fromDate!.month,
|
||||
fromDate!.day,
|
||||
toTime!.hour,
|
||||
toTime!.minute,
|
||||
);
|
||||
} else {
|
||||
// For regular leave, use dates at midnight
|
||||
finalStartDate = DateTime(fromDate!.year, fromDate!.month, fromDate!.day);
|
||||
finalEndDate = DateTime(toDate!.year, toDate!.month, toDate!.day);
|
||||
}
|
||||
|
||||
// Get vacation type value
|
||||
final typeValue = _getVacationTypeValue();
|
||||
|
||||
// Show loading indicator
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Save the leave request
|
||||
await _requestService.addLeaveRequest(leaveRequest);
|
||||
|
||||
// Show a success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم إرسال طلب الأجازة بنجاح'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
// Create vacation request
|
||||
final vacationRequest = VacationRequest(
|
||||
employeeId: employeeId,
|
||||
startDate: finalStartDate,
|
||||
endDate: finalEndDate,
|
||||
reason: reasonController.text,
|
||||
type: typeValue,
|
||||
);
|
||||
|
||||
// Navigate back to the previous screen
|
||||
Navigator.pop(context);
|
||||
final result = await _createVacationUseCase(vacationRequest);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // Close loading dialog
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_getFailureMessage(failure)),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
},
|
||||
(response) {
|
||||
// Also save locally for UI display
|
||||
final leaveRequest = LeaveRequest(
|
||||
id: response.data?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
leaveType: leaveType,
|
||||
isTimedLeave: isTimedLeave,
|
||||
fromDate: fromDate!,
|
||||
toDate: toDate!,
|
||||
fromTime: fromTime!,
|
||||
toTime: toTime!,
|
||||
reason: reasonController.text,
|
||||
requestDate: DateTime.now(),
|
||||
status: "waiting", // Default status
|
||||
);
|
||||
_requestService.addLeaveRequest(leaveRequest);
|
||||
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم إرسال طلب الأجازة بنجاح'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate back to the previous screen
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Show an error message if something went wrong
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('حدث خطأ: $e'), backgroundColor: Colors.red),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // Close loading dialog
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('حدث خطأ غير متوقع: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,68 +390,51 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
|
||||
],
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: leaveType,
|
||||
icon: const Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
color: Colors.black,
|
||||
size: 28,
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 17,
|
||||
),
|
||||
isExpanded: true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
leaveType = value!;
|
||||
// Set toggle based on selected value
|
||||
isTimedLeave = value == "أجازة زمنية";
|
||||
});
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: "إجازة مرضية ",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("إجازة مرضية "),
|
||||
child: _vacationTypes.isEmpty
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "إجازة مدفوعة",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("إجازة مدفوعة"),
|
||||
)
|
||||
: DropdownButton<int>(
|
||||
value: _selectedVacationTypeValue,
|
||||
icon: const Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
color: Colors.black,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "إجازة غير مدفوعة",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("إجازة غير مدفوعة"),
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 17,
|
||||
),
|
||||
isExpanded: true,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedVacationTypeValue = value;
|
||||
final selectedType = _vacationTypes
|
||||
.firstWhere((t) => t.value == value);
|
||||
leaveType = _getArabicName(selectedType.name);
|
||||
// Set toggle based on selected value (TimeOff = 1)
|
||||
isTimedLeave = value == 1;
|
||||
});
|
||||
}
|
||||
},
|
||||
items: _vacationTypes.map((type) {
|
||||
final arabicName = _getArabicName(type.name);
|
||||
return DropdownMenuItem<int>(
|
||||
value: type.value,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(arabicName),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "أجازة زمنية",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("أجازة زمنية"),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -293,9 +450,12 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
|
||||
onTap: () {
|
||||
setState(() {
|
||||
isTimedLeave = !isTimedLeave;
|
||||
// Set leave type to "أجازة زمنية" when toggle is ON
|
||||
// Set leave type to TimeOff (value: 1) when toggle is ON
|
||||
if (isTimedLeave) {
|
||||
leaveType = "أجازة زمنية";
|
||||
final timeOffType = _vacationTypes
|
||||
.firstWhere((t) => t.value == 1, orElse: () => _vacationTypes.first);
|
||||
_selectedVacationTypeValue = timeOffType.value;
|
||||
leaveType = _getArabicName(timeOffType.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'onboarding_screen.dart';
|
||||
import 'main_screen.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
@@ -14,13 +17,31 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
FlutterNativeSplash.remove();
|
||||
_checkTokenAndNavigate();
|
||||
}
|
||||
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
Future<void> _checkTokenAndNavigate() async {
|
||||
// Wait for splash screen display
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Check if token exists in cache
|
||||
final token = await sl<UserLocalDataSource>().getCachedUserToken();
|
||||
|
||||
if (token != null && token.isNotEmpty) {
|
||||
// Token exists, navigate directly to MainPage
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => OnboardingScreen()),
|
||||
MaterialPageRoute(builder: (_) => const MainPage()),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// No token, navigate to OnboardingScreen
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const OnboardingScreen()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../screens/about_screen.dart';
|
||||
import '../screens/auth_screen.dart';
|
||||
import 'about_screen.dart';
|
||||
import 'auth_screen.dart';
|
||||
import '../widgets/change_password_modal.dart';
|
||||
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
|
||||
class UserSettingsScreen extends StatefulWidget {
|
||||
const UserSettingsScreen({super.key});
|
||||
@@ -143,8 +144,8 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
|
||||
duration: const Duration(
|
||||
milliseconds: 250,
|
||||
),
|
||||
width: 75,
|
||||
height: 30,
|
||||
width: 75,
|
||||
height: 30,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
),
|
||||
@@ -227,13 +228,18 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
|
||||
_settingsRow(
|
||||
label: "تسجيل خروج",
|
||||
icon: "assets/images/logout2.svg",
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const AuthScreen(),
|
||||
),
|
||||
);
|
||||
onTap: () async {
|
||||
await sl<UserLocalDataSource>()
|
||||
.clearCache();
|
||||
if (context.mounted) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const AuthScreen(),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
318
lib/presentation/widgets/auth_form.dart
Normal file
318
lib/presentation/widgets/auth_form.dart
Normal file
@@ -0,0 +1,318 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../screens/main_screen.dart';
|
||||
import '../../domain/models/login_request.dart';
|
||||
import '../blocs/login/login_bloc.dart';
|
||||
import '../blocs/login/login_event.dart';
|
||||
import '../blocs/login/login_state.dart';
|
||||
import 'onboarding_button.dart';
|
||||
|
||||
class AuthForm extends StatefulWidget {
|
||||
final VoidCallback? onSubmit;
|
||||
|
||||
const AuthForm({super.key, this.onSubmit});
|
||||
|
||||
@override
|
||||
State<AuthForm> createState() => _AuthFormState();
|
||||
}
|
||||
|
||||
class _AuthFormState extends State<AuthForm> {
|
||||
bool _obscure = true;
|
||||
|
||||
// Text controllers
|
||||
final TextEditingController _phoneNumberController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
// Focus nodes for text fields
|
||||
late FocusNode _phoneNumberFocusNode;
|
||||
late FocusNode _passwordFocusNode;
|
||||
|
||||
void _handleLogin() {
|
||||
// Validate inputs
|
||||
if (_phoneNumberController.text.trim().isEmpty) {
|
||||
_showError('الرجاء إدخال رقم الهاتف');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_passwordController.text.trim().isEmpty) {
|
||||
_showError('الرجاء إدخال كلمة المرور');
|
||||
return;
|
||||
}
|
||||
|
||||
// Unfocus any focused text field
|
||||
_phoneNumberFocusNode.unfocus();
|
||||
_passwordFocusNode.unfocus();
|
||||
|
||||
// Dispatch login event
|
||||
final request = LoginRequest(
|
||||
phoneNumber: _phoneNumberController.text.trim(),
|
||||
password: _passwordController.text.trim(),
|
||||
);
|
||||
|
||||
context.read<LoginBloc>().add(LoginSubmitted(request));
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize focus nodes
|
||||
_phoneNumberFocusNode = FocusNode();
|
||||
_passwordFocusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Clean up controllers and focus nodes
|
||||
_phoneNumberController.dispose();
|
||||
_passwordController.dispose();
|
||||
_phoneNumberFocusNode.dispose();
|
||||
_passwordFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get screen dimensions
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final screenWidth = screenSize.width;
|
||||
final screenHeight = screenSize.height;
|
||||
|
||||
// Calculate responsive dimensions
|
||||
final formWidth = screenWidth > 600 ? screenWidth * 0.5 : screenWidth * 0.9;
|
||||
final formHeight =
|
||||
screenHeight > 800 ? screenHeight * 0.6 : screenHeight * 0.8;
|
||||
final borderWidth = formWidth + 20;
|
||||
final titleFontSize = screenWidth > 600 ? 28.0 : 24.0;
|
||||
final labelFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final fieldFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final verticalSpacing = screenHeight > 800 ? 34.0 : 24.0;
|
||||
final fieldSpacing = screenHeight > 800 ? 30.0 : 20.0;
|
||||
final buttonSpacing = screenHeight > 800 ? 100.0 : 60.0;
|
||||
final bottomSpacing = screenHeight > 800 ? 40.0 : 20.0;
|
||||
final horizontalPadding = screenWidth > 600 ? 30.0 : 25.0;
|
||||
final verticalPadding = screenHeight > 800 ? 38.0 : 28.0;
|
||||
|
||||
return BlocListener<LoginBloc, LoginState>(
|
||||
listener: (context, state) {
|
||||
if (state is LoginSuccess) {
|
||||
// Call the onSubmit callback if provided
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!();
|
||||
}
|
||||
|
||||
// Navigate to the MainPage
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MainPage()),
|
||||
);
|
||||
} else if (state is LoginError) {
|
||||
_showError(state.message);
|
||||
}
|
||||
},
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: FocusScope(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Border container - decorative element behind the form
|
||||
Container(
|
||||
width: borderWidth,
|
||||
constraints: BoxConstraints(
|
||||
minHeight: formHeight + 40,
|
||||
maxHeight:
|
||||
formHeight + 80, // Allows shrinking when keyboard opens
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
border: Border.all(color: const Color(0xDD00C28E), width: 1),
|
||||
),
|
||||
),
|
||||
|
||||
// Main form container
|
||||
Container(
|
||||
width: formWidth,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: horizontalPadding,
|
||||
vertical: verticalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEEFFFA),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
/// Title
|
||||
Center(
|
||||
child: Text(
|
||||
"تسجيل دخول",
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: verticalSpacing),
|
||||
|
||||
/// Phone Number Label
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"رقم الهاتف",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
controller: _phoneNumberController,
|
||||
hint: "رقم الهاتف",
|
||||
obscure: false,
|
||||
keyboardType: TextInputType.phone,
|
||||
focusNode: _phoneNumberFocusNode,
|
||||
textInputAction: TextInputAction.next,
|
||||
onSubmitted: (_) {
|
||||
// Move focus to password field when next is pressed
|
||||
FocusScope.of(context).requestFocus(_passwordFocusNode);
|
||||
},
|
||||
fontSize: fieldFontSize,
|
||||
),
|
||||
|
||||
SizedBox(height: fieldSpacing),
|
||||
|
||||
/// Password Label
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"كلمة المرور",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
controller: _passwordController,
|
||||
hint: "كلمة المرور",
|
||||
obscure: _obscure,
|
||||
hasEye: true,
|
||||
focusNode: _passwordFocusNode,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _handleLogin(),
|
||||
fontSize: fieldFontSize,
|
||||
),
|
||||
|
||||
SizedBox(height: buttonSpacing), // Responsive spacing
|
||||
|
||||
BlocBuilder<LoginBloc, LoginState>(
|
||||
builder: (context, state) {
|
||||
final isLoading = state is LoginLoading;
|
||||
return Center(
|
||||
child: OnboardingButton(
|
||||
text:
|
||||
isLoading
|
||||
? "جاري تسجيل الدخول..."
|
||||
: "تسجيل دخول",
|
||||
backgroundColor: const Color.fromARGB(
|
||||
239,
|
||||
35,
|
||||
87,
|
||||
74,
|
||||
),
|
||||
onPressed: isLoading ? null : _handleLogin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
SizedBox(height: bottomSpacing),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField({
|
||||
TextEditingController? controller,
|
||||
required String hint,
|
||||
required bool obscure,
|
||||
bool hasEye = false,
|
||||
TextInputType? keyboardType,
|
||||
FocusNode? focusNode,
|
||||
TextInputAction? textInputAction,
|
||||
Function(String)? onSubmitted,
|
||||
required double fontSize,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xDEDEDEDE),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
obscureText: obscure,
|
||||
keyboardType: keyboardType,
|
||||
textAlign: TextAlign.right,
|
||||
textInputAction: textInputAction,
|
||||
onSubmitted: onSubmitted,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.black54),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
suffixIcon:
|
||||
hasEye
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
obscure ? Icons.visibility_off : Icons.visibility,
|
||||
color: Colors.black54,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _obscure = !obscure);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/onboarding_button.dart';
|
||||
import 'onboarding_button.dart';
|
||||
|
||||
class ChangePasswordModal extends StatefulWidget {
|
||||
const ChangePasswordModal({super.key});
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import 'app_background.dart';
|
||||
|
||||
class LoginAnimationScreen extends StatefulWidget {
|
||||
final bool isLogin;
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import '../widgets/gradient_line.dart';
|
||||
import '../widgets/status_circle.dart';
|
||||
import 'gradient_line.dart';
|
||||
import 'status_circle.dart';
|
||||
|
||||
class WorkDayCard extends StatelessWidget {
|
||||
const WorkDayCard({super.key});
|
||||
@@ -1,28 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/auth_form.dart';
|
||||
|
||||
class AuthScreen extends StatelessWidget {
|
||||
const AuthScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: AppBackground(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
// Logo
|
||||
Center(child: Image.asset("assets/images/logo2.png", width: 200)),
|
||||
// const SizedBox(height: 15),
|
||||
// Form - taking remaining space and centered
|
||||
Expanded(child: Center(child: const AuthForm())),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../screens/main_screen.dart';
|
||||
import '../core/di/injection_container.dart';
|
||||
import '../domain/usecases/login_usecase.dart';
|
||||
import '../domain/models/login_request.dart';
|
||||
import 'onboarding_button.dart';
|
||||
|
||||
class AuthForm extends StatefulWidget {
|
||||
final VoidCallback? onSubmit;
|
||||
|
||||
const AuthForm({super.key, this.onSubmit});
|
||||
|
||||
@override
|
||||
State<AuthForm> createState() => _AuthFormState();
|
||||
}
|
||||
|
||||
class _AuthFormState extends State<AuthForm> {
|
||||
bool _obscure = true;
|
||||
bool _isLoading = false;
|
||||
|
||||
// Text controllers
|
||||
final TextEditingController _phoneNumberController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
// Focus nodes for text fields
|
||||
late FocusNode _phoneNumberFocusNode;
|
||||
late FocusNode _passwordFocusNode;
|
||||
|
||||
// Get LoginUseCase from dependency injection
|
||||
final LoginUseCase _loginUseCase = sl<LoginUseCase>();
|
||||
|
||||
Future<void> _handleLogin() async {
|
||||
// Validate inputs
|
||||
if (_phoneNumberController.text.trim().isEmpty) {
|
||||
_showError('الرجاء إدخال رقم الهاتف');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_passwordController.text.trim().isEmpty) {
|
||||
_showError('الرجاء إدخال كلمة المرور');
|
||||
return;
|
||||
}
|
||||
|
||||
// Unfocus any focused text field
|
||||
_phoneNumberFocusNode.unfocus();
|
||||
_passwordFocusNode.unfocus();
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final request = LoginRequest(
|
||||
phoneNumber: _phoneNumberController.text.trim(),
|
||||
password: _passwordController.text.trim(),
|
||||
);
|
||||
|
||||
final result = await _loginUseCase(request);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_showError(failure.message);
|
||||
},
|
||||
(response) {
|
||||
if (response.isSuccess) {
|
||||
// Call the onSubmit callback if provided
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!();
|
||||
}
|
||||
|
||||
// Navigate to the MainPage
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MainPage()),
|
||||
);
|
||||
} else {
|
||||
_showError(response.message);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_showError('حدث خطأ غير متوقع: $e');
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize focus nodes
|
||||
_phoneNumberFocusNode = FocusNode();
|
||||
_passwordFocusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Clean up controllers and focus nodes
|
||||
_phoneNumberController.dispose();
|
||||
_passwordController.dispose();
|
||||
_phoneNumberFocusNode.dispose();
|
||||
_passwordFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get screen dimensions
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final screenWidth = screenSize.width;
|
||||
final screenHeight = screenSize.height;
|
||||
|
||||
// Calculate responsive dimensions
|
||||
final formWidth = screenWidth > 600 ? screenWidth * 0.5 : screenWidth * 0.9;
|
||||
final formHeight =
|
||||
screenHeight > 800 ? screenHeight * 0.6 : screenHeight * 0.8;
|
||||
final borderWidth = formWidth + 20;
|
||||
final titleFontSize = screenWidth > 600 ? 28.0 : 24.0;
|
||||
final labelFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final fieldFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final verticalSpacing = screenHeight > 800 ? 34.0 : 24.0;
|
||||
final fieldSpacing = screenHeight > 800 ? 30.0 : 20.0;
|
||||
final buttonSpacing = screenHeight > 800 ? 100.0 : 60.0;
|
||||
final bottomSpacing = screenHeight > 800 ? 40.0 : 20.0;
|
||||
final horizontalPadding = screenWidth > 600 ? 30.0 : 25.0;
|
||||
final verticalPadding = screenHeight > 800 ? 38.0 : 28.0;
|
||||
|
||||
return Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: FocusScope(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Border container - decorative element behind the form
|
||||
Container(
|
||||
width: borderWidth,
|
||||
constraints: BoxConstraints(
|
||||
minHeight: formHeight + 40,
|
||||
maxHeight:
|
||||
formHeight + 80, // Allows shrinking when keyboard opens
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
border: Border.all(color: const Color(0xDD00C28E), width: 1),
|
||||
),
|
||||
),
|
||||
|
||||
// Main form container
|
||||
Container(
|
||||
width: formWidth,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: horizontalPadding,
|
||||
vertical: verticalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEEFFFA),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
/// Title
|
||||
Center(
|
||||
child: Text(
|
||||
"تسجيل دخول",
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: verticalSpacing),
|
||||
|
||||
/// Phone Number Label
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"رقم الهاتف",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
controller: _phoneNumberController,
|
||||
hint: "رقم الهاتف",
|
||||
obscure: false,
|
||||
keyboardType: TextInputType.phone,
|
||||
focusNode: _phoneNumberFocusNode,
|
||||
textInputAction: TextInputAction.next,
|
||||
onSubmitted: (_) {
|
||||
// Move focus to password field when next is pressed
|
||||
FocusScope.of(context).requestFocus(_passwordFocusNode);
|
||||
},
|
||||
fontSize: fieldFontSize,
|
||||
),
|
||||
|
||||
SizedBox(height: fieldSpacing),
|
||||
|
||||
/// Password Label
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"كلمة المرور",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
controller: _passwordController,
|
||||
hint: "كلمة المرور",
|
||||
obscure: _obscure,
|
||||
hasEye: true,
|
||||
focusNode: _passwordFocusNode,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _handleLogin(),
|
||||
fontSize: fieldFontSize,
|
||||
),
|
||||
|
||||
SizedBox(height: buttonSpacing), // Responsive spacing
|
||||
|
||||
Center(
|
||||
child: OnboardingButton(
|
||||
text: _isLoading ? "جاري تسجيل الدخول..." : "تسجيل دخول",
|
||||
backgroundColor: const Color.fromARGB(239, 35, 87, 74),
|
||||
onPressed: _isLoading ? null : _handleLogin,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: bottomSpacing),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField({
|
||||
TextEditingController? controller,
|
||||
required String hint,
|
||||
required bool obscure,
|
||||
bool hasEye = false,
|
||||
TextInputType? keyboardType,
|
||||
FocusNode? focusNode,
|
||||
TextInputAction? textInputAction,
|
||||
Function(String)? onSubmitted,
|
||||
required double fontSize,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xDEDEDEDE),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
obscureText: obscure,
|
||||
keyboardType: keyboardType,
|
||||
textAlign: TextAlign.right,
|
||||
textInputAction: textInputAction,
|
||||
onSubmitted: onSubmitted,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.black54),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
suffixIcon:
|
||||
hasEye
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
obscure ? Icons.visibility_off : Icons.visibility,
|
||||
color: Colors.black54,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _obscure = !obscure);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
50
pubspec.lock
50
pubspec.lock
@@ -29,10 +29,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
version: "2.13.0"
|
||||
bloc:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bloc
|
||||
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.4"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -181,10 +189,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -206,6 +214,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bloc
|
||||
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.6"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -273,7 +289,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
@@ -308,10 +324,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "10.0.9"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -368,6 +384,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -440,6 +464,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -609,10 +641,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.1"
|
||||
version: "15.0.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -17,6 +17,7 @@ dependencies:
|
||||
dartz: ^0.10.1
|
||||
equatable: ^2.0.5
|
||||
shared_preferences: ^2.2.2
|
||||
flutter_bloc: ^8.1.6
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user