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,12 +1,29 @@
|
|||||||
|
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:dio/dio.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../network/api_client.dart';
|
import '../network/api_client.dart';
|
||||||
import '../../data/datasources/auth_remote_data_source.dart';
|
import '../../data/datasources/auth_remote_data_source.dart';
|
||||||
import '../../data/datasources/user_local_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/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/auth_repository.dart';
|
||||||
|
import '../../domain/repositories/vacation_repository.dart';
|
||||||
|
import '../../domain/repositories/advance_repository.dart';
|
||||||
import '../../domain/usecases/login_usecase.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;
|
final sl = GetIt.instance;
|
||||||
|
|
||||||
@@ -34,16 +51,50 @@ Future<void> initializeDependencies() async {
|
|||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
sl.registerLazySingleton<AuthRepository>(
|
sl.registerLazySingleton<AuthRepository>(
|
||||||
() => AuthRepositoryImpl(
|
() => AuthRepositoryImpl(remoteDataSource: sl(), localDataSource: sl()),
|
||||||
remoteDataSource: sl(),
|
|
||||||
localDataSource: sl(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use cases
|
// Use cases
|
||||||
sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
|
sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
|
||||||
|
|
||||||
// Blocs will be registered here
|
// Blocs
|
||||||
// Example:
|
sl.registerFactory(() => LoginBloc(loginUseCase: sl()));
|
||||||
// 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 'dart:async';
|
||||||
import '../models/leave_request.dart';
|
import '../../models/leave_request.dart';
|
||||||
import '../models/advance_request.dart';
|
import '../../models/advance_request.dart';
|
||||||
|
|
||||||
class RequestService {
|
class RequestService {
|
||||||
// Singleton implementation
|
// 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
|
@override
|
||||||
Future<LoginResponseDto> login(LoginDto dto) async {
|
Future<LoginResponseDto> login(LoginDto dto) async {
|
||||||
try {
|
try {
|
||||||
final response = await apiClient.post(
|
final response = await apiClient.post('/Auth/login', data: dto.toJson());
|
||||||
'/Auth/login',
|
|
||||||
data: dto.toJson(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
final responseData = response.data;
|
final responseData = response.data;
|
||||||
@@ -47,7 +44,8 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
|||||||
} else if (e.response?.statusCode == 500) {
|
} else if (e.response?.statusCode == 500) {
|
||||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||||
} else if (e.response != null) {
|
} else if (e.response != null) {
|
||||||
final message = e.response?.data?['message'] ??
|
final message =
|
||||||
|
e.response?.data?['message'] ??
|
||||||
e.response?.data?['error'] ??
|
e.response?.data?['error'] ??
|
||||||
'فشل تسجيل الدخول';
|
'فشل تسجيل الدخول';
|
||||||
|
|
||||||
@@ -57,8 +55,8 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
|||||||
message.toString().toLowerCase().contains('incorrect')
|
message.toString().toLowerCase().contains('incorrect')
|
||||||
? 'رقم الهاتف أو كلمة المرور غير صحيحة'
|
? 'رقم الهاتف أو كلمة المرور غير صحيحة'
|
||||||
: message.toString().toLowerCase().contains('not found')
|
: message.toString().toLowerCase().contains('not found')
|
||||||
? 'المستخدم غير موجود'
|
? 'المستخدم غير موجود'
|
||||||
: message;
|
: message;
|
||||||
|
|
||||||
throw ServerException(
|
throw ServerException(
|
||||||
message: customMessage,
|
message: customMessage,
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ abstract class UserLocalDataSource {
|
|||||||
Future<void> cacheUserToken(String token);
|
Future<void> cacheUserToken(String token);
|
||||||
Future<String?> getCachedUserToken();
|
Future<String?> getCachedUserToken();
|
||||||
Future<void> clearCache();
|
Future<void> clearCache();
|
||||||
|
Future<void> cacheEmployeeId(String id);
|
||||||
|
Future<String?> getCachedEmployeeId();
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserLocalDataSourceImpl implements UserLocalDataSource {
|
class UserLocalDataSourceImpl implements UserLocalDataSource {
|
||||||
final SharedPreferences sharedPreferences;
|
final SharedPreferences sharedPreferences;
|
||||||
static const String _tokenKey = 'user_token';
|
static const String _tokenKey = 'user_token';
|
||||||
|
static const String _employeeIdKey = 'employee_id';
|
||||||
|
|
||||||
UserLocalDataSourceImpl({required this.sharedPreferences});
|
UserLocalDataSourceImpl({required this.sharedPreferences});
|
||||||
|
|
||||||
@@ -25,5 +28,16 @@ class UserLocalDataSourceImpl implements UserLocalDataSource {
|
|||||||
@override
|
@override
|
||||||
Future<void> clearCache() async {
|
Future<void> clearCache() async {
|
||||||
await sharedPreferences.remove(_tokenKey);
|
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(
|
return LoginDataDto(
|
||||||
token: json['token'],
|
token: json['token'],
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
employeeId: json['employeeId'],
|
employeeId:
|
||||||
|
json['employeeId'] ?? json['EmployeeId'] ?? json['employee_id'],
|
||||||
username: json['username'],
|
username: json['username'],
|
||||||
fullName: json['fullName'],
|
fullName: json['fullName'],
|
||||||
role: json['role'],
|
role: json['role'],
|
||||||
email: json['email'],
|
email: json['email'],
|
||||||
phoneNumber: json['phoneNumber'],
|
phoneNumber: json['phoneNumber'],
|
||||||
permissions: json['permissions'] != null
|
permissions:
|
||||||
? List<String>.from(json['permissions'])
|
json['permissions'] != null
|
||||||
: 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/auth_remote_data_source.dart';
|
||||||
import '../datasources/user_local_data_source.dart';
|
import '../datasources/user_local_data_source.dart';
|
||||||
import '../dto/login_dto.dart';
|
import '../dto/login_dto.dart';
|
||||||
import '../dto/login_response_dto.dart';
|
|
||||||
import '../../domain/models/login_request.dart';
|
import '../../domain/models/login_request.dart';
|
||||||
import '../../domain/models/login_response_model.dart';
|
import '../../domain/models/login_response_model.dart';
|
||||||
import '../../domain/repositories/auth_repository.dart';
|
import '../../domain/repositories/auth_repository.dart';
|
||||||
@@ -19,7 +18,9 @@ class AuthRepositoryImpl implements AuthRepository {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Either<Failure, LoginResponseModel>> login(LoginRequest request) async {
|
Future<Either<Failure, LoginResponseModel>> login(
|
||||||
|
LoginRequest request,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
final dto = LoginDto(
|
final dto = LoginDto(
|
||||||
phoneNumber: request.phoneNumber,
|
phoneNumber: request.phoneNumber,
|
||||||
@@ -27,30 +28,38 @@ class AuthRepositoryImpl implements AuthRepository {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final responseDto = await remoteDataSource.login(dto);
|
final responseDto = await remoteDataSource.login(dto);
|
||||||
|
print("LOGIN RESPONSE DATA: ${responseDto.toJson()}"); // Debugging Log
|
||||||
|
|
||||||
// Cache the token locally
|
// Cache the token locally
|
||||||
if (responseDto.data?.token != null) {
|
if (responseDto.data?.token != null) {
|
||||||
await localDataSource.cacheUserToken(responseDto.data!.token!);
|
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
|
// Convert DTO to Model
|
||||||
final responseModel = LoginResponseModel(
|
final responseModel = LoginResponseModel(
|
||||||
statusCode: responseDto.statusCode,
|
statusCode: responseDto.statusCode,
|
||||||
isSuccess: responseDto.isSuccess,
|
isSuccess: responseDto.isSuccess,
|
||||||
message: responseDto.message,
|
message: responseDto.message,
|
||||||
data: responseDto.data != null
|
data:
|
||||||
? LoginDataModel(
|
responseDto.data != null
|
||||||
token: responseDto.data!.token,
|
? LoginDataModel(
|
||||||
id: responseDto.data!.id,
|
token: responseDto.data!.token,
|
||||||
employeeId: responseDto.data!.employeeId,
|
id: responseDto.data!.id,
|
||||||
username: responseDto.data!.username,
|
employeeId: responseDto.data!.employeeId,
|
||||||
fullName: responseDto.data!.fullName,
|
username: responseDto.data!.username,
|
||||||
role: responseDto.data!.role,
|
fullName: responseDto.data!.fullName,
|
||||||
email: responseDto.data!.email,
|
role: responseDto.data!.role,
|
||||||
phoneNumber: responseDto.data!.phoneNumber,
|
email: responseDto.data!.email,
|
||||||
permissions: responseDto.data!.permissions,
|
phoneNumber: responseDto.data!.phoneNumber,
|
||||||
)
|
permissions: responseDto.data!.permissions,
|
||||||
: null,
|
)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Right(responseModel);
|
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 phoneNumber;
|
||||||
final String password;
|
final String password;
|
||||||
|
|
||||||
LoginRequest({
|
LoginRequest({required this.phoneNumber, required this.password});
|
||||||
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 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
|
|
||||||
import 'core/di/injection_container.dart';
|
import 'core/di/injection_container.dart';
|
||||||
import 'screens/splash_screen.dart';
|
import 'presentation/screens/splash_screen.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
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/presentation/screens/face_screen.dart';
|
||||||
import 'package:coda_project/screens/notifications_screen.dart';
|
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||||
import 'package:coda_project/screens/user_settings_screen.dart';
|
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import '../widgets/settings_bar.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 {
|
class AttendanceScreen extends StatelessWidget {
|
||||||
const AttendanceScreen({super.key});
|
const AttendanceScreen({super.key});
|
||||||
@@ -138,12 +144,40 @@ class AttendanceScreen extends StatelessWidget {
|
|||||||
child: _FingerButton(
|
child: _FingerButton(
|
||||||
icon: "assets/images/faceLogin.svg",
|
icon: "assets/images/faceLogin.svg",
|
||||||
label: "تسجيل الدخول",
|
label: "تسجيل الدخول",
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
Navigator.of(context).push(
|
final employeeId =
|
||||||
MaterialPageRoute(
|
await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||||
builder: (_) => OvalCameraCapturePage(isLogin: true),
|
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(
|
child: _FingerButton(
|
||||||
icon: "assets/images/faceLogout.svg",
|
icon: "assets/images/faceLogout.svg",
|
||||||
label: "تسجيل خروج",
|
label: "تسجيل خروج",
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
Navigator.of(context).push(
|
final employeeId =
|
||||||
MaterialPageRoute(
|
await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||||
builder: (_) => OvalCameraCapturePage(isLogin: false),
|
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/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import '../../core/error/exceptions.dart';
|
||||||
|
|
||||||
class OvalCameraCapturePage extends StatefulWidget {
|
class OvalCameraCapturePage extends StatefulWidget {
|
||||||
final bool isLogin;
|
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
|
@override
|
||||||
State<OvalCameraCapturePage> createState() => _OvalCameraCapturePageState();
|
State<OvalCameraCapturePage> createState() => _OvalCameraCapturePageState();
|
||||||
@@ -16,6 +24,7 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|||||||
bool _isCameraInitialized = false;
|
bool _isCameraInitialized = false;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
bool _isSuccess = false;
|
bool _isSuccess = false;
|
||||||
|
bool _isLoading = false;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -26,6 +35,11 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|||||||
|
|
||||||
Future<void> _initializeCamera() async {
|
Future<void> _initializeCamera() async {
|
||||||
try {
|
try {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = null;
|
||||||
|
_isCameraInitialized = false;
|
||||||
|
});
|
||||||
|
|
||||||
// Dispose existing controller if any
|
// Dispose existing controller if any
|
||||||
await _cameraController?.dispose();
|
await _cameraController?.dispose();
|
||||||
_cameraController = null;
|
_cameraController = null;
|
||||||
@@ -35,8 +49,9 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|||||||
|
|
||||||
// Check if cameras list is available
|
// Check if cameras list is available
|
||||||
if (cameras.isEmpty) {
|
if (cameras.isEmpty) {
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage = "لا توجد كاميرات متاحة";
|
_errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز";
|
||||||
_isCameraInitialized = false;
|
_isCameraInitialized = false;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -53,8 +68,9 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|||||||
if (cameras.isNotEmpty) {
|
if (cameras.isNotEmpty) {
|
||||||
selectedCamera = cameras.first;
|
selectedCamera = cameras.first;
|
||||||
} else {
|
} else {
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage = "لا توجد كاميرات متاحة";
|
_errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز";
|
||||||
_isCameraInitialized = false;
|
_isCameraInitialized = false;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -77,31 +93,143 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
_timer = Timer(const Duration(seconds: 3), () {
|
_startScan();
|
||||||
if (mounted) {
|
} on CameraException catch (e) {
|
||||||
setState(() {
|
if (!mounted) return;
|
||||||
_isSuccess = true;
|
String errorMessage;
|
||||||
});
|
switch (e.code) {
|
||||||
|
case 'CameraAccessDenied':
|
||||||
// Auto-close after 2 seconds
|
errorMessage = "تم رفض الوصول إلى الكاميرا. يرجى السماح بالوصول في الإعدادات";
|
||||||
Future.delayed(const Duration(seconds: 2), () {
|
break;
|
||||||
if (mounted) {
|
case 'CameraAccessDeniedWithoutPrompt':
|
||||||
Navigator.of(context).pop();
|
errorMessage = "تم رفض الوصول إلى الكاميرا. يرجى السماح بالوصول في الإعدادات";
|
||||||
}
|
break;
|
||||||
});
|
case 'CameraAccessRestricted':
|
||||||
}
|
errorMessage = "الوصول إلى الكاميرا مقيد";
|
||||||
|
break;
|
||||||
|
case 'AudioAccessDenied':
|
||||||
|
errorMessage = "تم رفض الوصول إلى الميكروفون";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errorMessage = "خطأ في تهيئة الكاميرا: ${e.description ?? 'خطأ غير معروف'}";
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = errorMessage;
|
||||||
|
_isCameraInitialized = false;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage = "خطأ في تهيئة الكاميرا: $e";
|
_errorMessage = "حدث خطأ غير متوقع أثناء تهيئة الكاميرا";
|
||||||
_isCameraInitialized = false;
|
_isCameraInitialized = false;
|
||||||
});
|
});
|
||||||
print("Error initializing camera: $e");
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_cameraController?.dispose();
|
_cameraController?.dispose();
|
||||||
@@ -115,7 +243,8 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|||||||
backgroundColor: Color(0xff000000),
|
backgroundColor: Color(0xff000000),
|
||||||
|
|
||||||
body:
|
body:
|
||||||
_errorMessage != null
|
// Show error screen only if camera failed to initialize
|
||||||
|
_errorMessage != null && !_isCameraInitialized
|
||||||
? Center(
|
? Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
@@ -138,17 +267,39 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
SizedBox(height: 24),
|
SizedBox(height: 24),
|
||||||
ElevatedButton(
|
Row(
|
||||||
onPressed: _initializeCamera,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
style: ElevatedButton.styleFrom(
|
children: [
|
||||||
backgroundColor: Color(0xffE8001A),
|
ElevatedButton(
|
||||||
foregroundColor: Colors.white,
|
onPressed: _initializeCamera,
|
||||||
padding: EdgeInsets.symmetric(
|
style: ElevatedButton.styleFrom(
|
||||||
horizontal: 32,
|
backgroundColor: Color(0xffE8001A),
|
||||||
vertical: 12,
|
foregroundColor: Colors.white,
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 32,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text("إعادة المحاولة"),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(width: 16),
|
||||||
child: Text("إعادة المحاولة"),
|
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
|
||||||
? "تم تسجيل دخولك بنجاح"
|
? "تم تسجيل دخولك بنجاح"
|
||||||
: "تم تسجيل خروجك بنجاح")
|
: "تم تسجيل خروجك بنجاح")
|
||||||
: (widget.isLogin
|
: _isLoading
|
||||||
? "يتم تسجيل الدخول ..."
|
? (widget.isLogin
|
||||||
: "يتم تسجيل الخروج ..."),
|
? "يتم تسجيل الدخول ..."
|
||||||
|
: "يتم تسجيل الخروج ...")
|
||||||
|
: (widget.isLogin
|
||||||
|
? "جاهز للتقاط الصورة"
|
||||||
|
: "جاهز للتقاط الصورة"),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18,
|
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
|
// // Capture button
|
||||||
// Positioned(
|
// Positioned(
|
||||||
// bottom: 60,
|
// bottom: 60,
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:coda_project/screens/notifications_screen.dart';
|
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||||
import 'package:coda_project/screens/user_settings_screen.dart';
|
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../widgets/finance_summary_card.dart';
|
import '../widgets/finance_summary_card.dart';
|
||||||
import '../widgets/work_day_card.dart';
|
import '../widgets/work_day_card.dart';
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
import 'package:coda_project/screens/notifications_screen.dart';
|
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||||
import 'package:coda_project/screens/user_settings_screen.dart';
|
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
import '../widgets/settings_bar.dart';
|
import '../widgets/settings_bar.dart';
|
||||||
import '../screens/request_leave_screen.dart';
|
import 'request_leave_screen.dart';
|
||||||
import '../screens/request_advance_scrren.dart';
|
import 'request_advance_scrren.dart';
|
||||||
import '../models/leave_request.dart';
|
import '../../models/leave_request.dart';
|
||||||
import '../models/advance_request.dart';
|
import '../../models/advance_request.dart';
|
||||||
import '../services/request_service.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 {
|
class HolidayScreen extends StatefulWidget {
|
||||||
final void Function(bool isScrollingDown)? onScrollEvent;
|
final void Function(bool isScrollingDown)? onScrollEvent;
|
||||||
@@ -24,8 +32,12 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
|||||||
int activeTab = 0;
|
int activeTab = 0;
|
||||||
|
|
||||||
final RequestService _requestService = RequestService();
|
final RequestService _requestService = RequestService();
|
||||||
|
final GetVacationsUseCase _getVacationsUseCase = sl<GetVacationsUseCase>();
|
||||||
|
final GetAdvancesUseCase _getAdvancesUseCase = sl<GetAdvancesUseCase>();
|
||||||
List<LeaveRequest> _leaveRequests = [];
|
List<LeaveRequest> _leaveRequests = [];
|
||||||
List<AdvanceRequest> _advanceRequests = [];
|
List<AdvanceRequest> _advanceRequests = [];
|
||||||
|
bool _isLoadingVacations = false;
|
||||||
|
bool _isLoadingAdvances = false;
|
||||||
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
bool _showActions = true;
|
bool _showActions = true;
|
||||||
@@ -62,11 +74,18 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _initializeData() async {
|
void _initializeData() async {
|
||||||
_leaveRequests = await _requestService.getLeaveRequests();
|
// Load from API
|
||||||
_advanceRequests = await _requestService.getAdvanceRequests();
|
_loadVacationsFromAPI();
|
||||||
|
_loadAdvancesFromAPI();
|
||||||
|
|
||||||
|
// Also listen to local changes (for newly created requests)
|
||||||
_requestService.leaveRequestsStream.listen((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) {
|
_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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return Stack(
|
||||||
@@ -259,6 +412,19 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
|||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
Widget _buildLeaveRequestsSliver() {
|
Widget _buildLeaveRequestsSliver() {
|
||||||
|
if (_isLoadingVacations) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(40.0),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Color(0xFF8EFDC2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (_leaveRequests.isEmpty) {
|
if (_leaveRequests.isEmpty) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -287,6 +453,19 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAdvanceRequestsSliver() {
|
Widget _buildAdvanceRequestsSliver() {
|
||||||
|
if (_isLoadingAdvances) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(40.0),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Color(0xFF8EFDC2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (_advanceRequests.isEmpty) {
|
if (_advanceRequests.isEmpty) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../widgets/app_background.dart';
|
import '../widgets/app_background.dart';
|
||||||
import '../widgets/floatingnavbar.dart';
|
import '../widgets/FloatingNavBar.dart';
|
||||||
import '../screens/attendence_screen.dart';
|
import 'attendence_screen.dart';
|
||||||
import '../screens/finance_screen.dart';
|
import 'finance_screen.dart';
|
||||||
import '../screens/holiday_screen.dart';
|
import 'holiday_screen.dart';
|
||||||
|
|
||||||
class MainPage extends StatefulWidget {
|
class MainPage extends StatefulWidget {
|
||||||
const MainPage({super.key});
|
const MainPage({super.key});
|
||||||
@@ -15,15 +15,12 @@ class MainPage extends StatefulWidget {
|
|||||||
class _MainPageState extends State<MainPage> {
|
class _MainPageState extends State<MainPage> {
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final screenHeight = MediaQuery.sizeOf(context).height;
|
// final screenHeight = MediaQuery.sizeOf(context).height;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
/// BACKGROUND
|
/// BACKGROUND
|
||||||
const AppBackground(child: SizedBox()),
|
const AppBackground(child: SizedBox()),
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'dart:async';
|
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 'package:flutter/material.dart';
|
||||||
import '../widgets/onboarding_page.dart';
|
import '../widgets/onboarding_page.dart';
|
||||||
import '../widgets/onboarding_button.dart';
|
import '../widgets/onboarding_button.dart';
|
||||||
@@ -3,8 +3,13 @@ import 'package:flutter/material.dart';
|
|||||||
import '../widgets/app_background.dart';
|
import '../widgets/app_background.dart';
|
||||||
import '../widgets/settings_bar.dart';
|
import '../widgets/settings_bar.dart';
|
||||||
import '../widgets/onboarding_button.dart';
|
import '../widgets/onboarding_button.dart';
|
||||||
import '../models/advance_request.dart';
|
import '../../models/advance_request.dart';
|
||||||
import '../services/request_service.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 {
|
class RequestAdvanceScreen extends StatefulWidget {
|
||||||
const RequestAdvanceScreen({super.key});
|
const RequestAdvanceScreen({super.key});
|
||||||
@@ -23,16 +28,30 @@ class _RequestAdvanceScreenState extends State<RequestAdvanceScreen> {
|
|||||||
// Use the singleton instance
|
// Use the singleton instance
|
||||||
final RequestService _requestService = RequestService();
|
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
|
// Method to save the advance request
|
||||||
Future<void> _saveAdvanceRequest() async {
|
Future<void> _saveAdvanceRequest() async {
|
||||||
if (amountController.text.isEmpty || reasonController.text.isEmpty) {
|
if (amountController.text.isEmpty || reasonController.text.isEmpty) {
|
||||||
// Show an error message if fields are empty
|
// Show an error message if fields are empty
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
const SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text('الرجاء إدخال جميع الحقول'),
|
const SnackBar(
|
||||||
backgroundColor: Colors.red,
|
content: Text('الرجاء إدخال جميع الحقول'),
|
||||||
),
|
backgroundColor: Colors.red,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,45 +59,96 @@ class _RequestAdvanceScreenState extends State<RequestAdvanceScreen> {
|
|||||||
final amount = double.tryParse(amountController.text);
|
final amount = double.tryParse(amountController.text);
|
||||||
if (amount == null || amount <= 0) {
|
if (amount == null || amount <= 0) {
|
||||||
// Show an error message if amount is invalid
|
// Show an error message if amount is invalid
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
const SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text('الرجاء إدخال مبلغ صحيح'),
|
const SnackBar(
|
||||||
backgroundColor: Colors.red,
|
content: Text('الرجاء إدخال مبلغ صحيح'),
|
||||||
),
|
backgroundColor: Colors.red,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new advance request with default status "waiting"
|
// Get employee ID
|
||||||
final advanceRequest = AdvanceRequest(
|
final employeeId = await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
if (employeeId == null) {
|
||||||
amount: amount,
|
if (mounted) {
|
||||||
reason: reasonController.text,
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
status: "waiting", // Default status
|
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 {
|
try {
|
||||||
// Save the advance request
|
// Create advance request model
|
||||||
await _requestService.addAdvanceRequest(advanceRequest);
|
final advanceRequestModel = AdvanceRequestModel(
|
||||||
|
employeeId: employeeId,
|
||||||
// Show a success message
|
date: DateTime.now(),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
amount: amount,
|
||||||
const SnackBar(
|
reason: reasonController.text,
|
||||||
content: Text('تم إرسال طلب السلفة بنجاح'),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Navigate back to the previous screen
|
final result = await _createAdvanceUseCase(advanceRequestModel);
|
||||||
Navigator.pop(context);
|
|
||||||
|
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) {
|
} catch (e) {
|
||||||
// Show an error message if something went wrong
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
Navigator.pop(context); // Close loading dialog
|
||||||
SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text('حدث خطأ: $e'),
|
SnackBar(
|
||||||
backgroundColor: Colors.red,
|
content: Text('حدث خطأ غير متوقع: $e'),
|
||||||
),
|
backgroundColor: Colors.red,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3,8 +3,15 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||||||
import '../widgets/app_background.dart';
|
import '../widgets/app_background.dart';
|
||||||
import '../widgets/settings_bar.dart';
|
import '../widgets/settings_bar.dart';
|
||||||
import '../widgets/onboarding_button.dart';
|
import '../widgets/onboarding_button.dart';
|
||||||
import '../models/leave_request.dart';
|
import '../../models/leave_request.dart';
|
||||||
import '../services/request_service.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 {
|
class RequestLeaveScreen extends StatefulWidget {
|
||||||
const RequestLeaveScreen({super.key});
|
const RequestLeaveScreen({super.key});
|
||||||
@@ -33,6 +40,14 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
|
|||||||
// Use the singleton instance
|
// Use the singleton instance
|
||||||
final RequestService _requestService = RequestService();
|
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
|
/// PICK DATE
|
||||||
Future<void> pickDate(bool isFrom) async {
|
Future<void> pickDate(bool isFrom) async {
|
||||||
DateTime initial = isFrom ? fromDate! : toDate!;
|
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
|
// Method to save the leave request
|
||||||
Future<void> _saveLeaveRequest() async {
|
Future<void> _saveLeaveRequest() async {
|
||||||
if (reasonController.text.isEmpty) {
|
if (reasonController.text.isEmpty) {
|
||||||
// Show an error message if reason is empty
|
// Show an error message if reason is empty
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
const SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text('الرجاء إدخال السبب'),
|
const SnackBar(
|
||||||
backgroundColor: Colors.red,
|
content: Text('الرجاء إدخال السبب'),
|
||||||
),
|
backgroundColor: Colors.red,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new leave request with default status "waiting"
|
// Get employee ID
|
||||||
final leaveRequest = LeaveRequest(
|
final employeeId = await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
if (employeeId == null) {
|
||||||
leaveType: leaveType, // Use the current leaveType value
|
if (mounted) {
|
||||||
isTimedLeave: isTimedLeave,
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
fromDate: fromDate!,
|
const SnackBar(
|
||||||
toDate: toDate!,
|
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||||
fromTime: fromTime!,
|
backgroundColor: Colors.red,
|
||||||
toTime: toTime!,
|
),
|
||||||
reason: reasonController.text,
|
);
|
||||||
requestDate: DateTime.now(),
|
}
|
||||||
status: "waiting", // Default status
|
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 {
|
try {
|
||||||
// Save the leave request
|
// Create vacation request
|
||||||
await _requestService.addLeaveRequest(leaveRequest);
|
final vacationRequest = VacationRequest(
|
||||||
|
employeeId: employeeId,
|
||||||
// Show a success message
|
startDate: finalStartDate,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
endDate: finalEndDate,
|
||||||
const SnackBar(
|
reason: reasonController.text,
|
||||||
content: Text('تم إرسال طلب الأجازة بنجاح'),
|
type: typeValue,
|
||||||
backgroundColor: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Navigate back to the previous screen
|
final result = await _createVacationUseCase(vacationRequest);
|
||||||
Navigator.pop(context);
|
|
||||||
|
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) {
|
} catch (e) {
|
||||||
// Show an error message if something went wrong
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
Navigator.pop(context); // Close loading dialog
|
||||||
SnackBar(content: Text('حدث خطأ: $e'), backgroundColor: Colors.red),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
);
|
SnackBar(
|
||||||
|
content: Text('حدث خطأ غير متوقع: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,68 +390,51 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: DropdownButtonHideUnderline(
|
child: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<String>(
|
child: _vacationTypes.isEmpty
|
||||||
value: leaveType,
|
? const Center(
|
||||||
icon: const Icon(
|
child: Padding(
|
||||||
Icons.keyboard_arrow_down_rounded,
|
padding: EdgeInsets.all(8.0),
|
||||||
color: Colors.black,
|
child: CircularProgressIndicator(),
|
||||||
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("إجازة مرضية "),
|
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
),
|
: DropdownButton<int>(
|
||||||
DropdownMenuItem(
|
value: _selectedVacationTypeValue,
|
||||||
value: "إجازة مدفوعة",
|
icon: const Icon(
|
||||||
child: Directionality(
|
Icons.keyboard_arrow_down_rounded,
|
||||||
textDirection: TextDirection.rtl,
|
color: Colors.black,
|
||||||
child: Align(
|
size: 28,
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Text("إجازة مدفوعة"),
|
|
||||||
),
|
),
|
||||||
),
|
style: const TextStyle(
|
||||||
),
|
color: Colors.black,
|
||||||
DropdownMenuItem(
|
fontSize: 17,
|
||||||
value: "إجازة غير مدفوعة",
|
|
||||||
child: Directionality(
|
|
||||||
textDirection: TextDirection.rtl,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Text("إجازة غير مدفوعة"),
|
|
||||||
),
|
),
|
||||||
|
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: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
isTimedLeave = !isTimedLeave;
|
isTimedLeave = !isTimedLeave;
|
||||||
// Set leave type to "أجازة زمنية" when toggle is ON
|
// Set leave type to TimeOff (value: 1) when toggle is ON
|
||||||
if (isTimedLeave) {
|
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/material.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'onboarding_screen.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 {
|
class SplashScreen extends StatefulWidget {
|
||||||
const SplashScreen({super.key});
|
const SplashScreen({super.key});
|
||||||
@@ -14,13 +17,31 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
FlutterNativeSplash.remove();
|
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(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => OnboardingScreen()),
|
MaterialPageRoute(builder: (_) => const MainPage()),
|
||||||
);
|
);
|
||||||
});
|
} else {
|
||||||
|
// No token, navigate to OnboardingScreen
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const OnboardingScreen()),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import '../widgets/app_background.dart';
|
import '../widgets/app_background.dart';
|
||||||
import '../widgets/settings_bar.dart';
|
import '../widgets/settings_bar.dart';
|
||||||
import '../screens/about_screen.dart';
|
import 'about_screen.dart';
|
||||||
import '../screens/auth_screen.dart';
|
import 'auth_screen.dart';
|
||||||
import '../widgets/change_password_modal.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 {
|
class UserSettingsScreen extends StatefulWidget {
|
||||||
const UserSettingsScreen({super.key});
|
const UserSettingsScreen({super.key});
|
||||||
@@ -143,8 +144,8 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
|
|||||||
duration: const Duration(
|
duration: const Duration(
|
||||||
milliseconds: 250,
|
milliseconds: 250,
|
||||||
),
|
),
|
||||||
width: 75,
|
width: 75,
|
||||||
height: 30,
|
height: 30,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 4,
|
horizontal: 4,
|
||||||
),
|
),
|
||||||
@@ -227,13 +228,18 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
|
|||||||
_settingsRow(
|
_settingsRow(
|
||||||
label: "تسجيل خروج",
|
label: "تسجيل خروج",
|
||||||
icon: "assets/images/logout2.svg",
|
icon: "assets/images/logout2.svg",
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
Navigator.push(
|
await sl<UserLocalDataSource>()
|
||||||
context,
|
.clearCache();
|
||||||
MaterialPageRoute(
|
if (context.mounted) {
|
||||||
builder: (_) => const AuthScreen(),
|
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 'package:flutter/material.dart';
|
||||||
import '../widgets/onboarding_button.dart';
|
import 'onboarding_button.dart';
|
||||||
|
|
||||||
class ChangePasswordModal extends StatefulWidget {
|
class ChangePasswordModal extends StatefulWidget {
|
||||||
const ChangePasswordModal({super.key});
|
const ChangePasswordModal({super.key});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import '../widgets/app_background.dart';
|
import 'app_background.dart';
|
||||||
|
|
||||||
class LoginAnimationScreen extends StatefulWidget {
|
class LoginAnimationScreen extends StatefulWidget {
|
||||||
final bool isLogin;
|
final bool isLogin;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import '../widgets/gradient_line.dart';
|
import 'gradient_line.dart';
|
||||||
import '../widgets/status_circle.dart';
|
import 'status_circle.dart';
|
||||||
|
|
||||||
class WorkDayCard extends StatelessWidget {
|
class WorkDayCard extends StatelessWidget {
|
||||||
const WorkDayCard({super.key});
|
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
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -181,10 +189,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.3.3"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -206,6 +214,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -273,7 +289,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.6"
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
@@ -308,10 +324,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.8"
|
version: "10.0.9"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -368,6 +384,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
nested:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nested
|
||||||
|
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -440,6 +464,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
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:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -609,10 +641,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.3.1"
|
version: "15.0.0"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ dependencies:
|
|||||||
dartz: ^0.10.1
|
dartz: ^0.10.1
|
||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
|
flutter_bloc: ^8.1.6
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user