Compare commits

...

9 Commits

Author SHA1 Message Date
Mohammed Al-Samarraie
cd7ba8e9d5 ٢٢٢ 2026-01-18 19:54:07 +03:00
Mohammed Al-Samarraie
33099c4497 1111 2026-01-18 19:52:10 +03:00
Mohammed Al-Samarraie
79b53b6303 1111 2026-01-18 19:40:54 +03:00
Mohammed Al-Samarraie
8adab4c4af done 2026-01-16 14:58:12 +03:00
Mohammed Al-Samarraie
2fd5aff0c2 1111 2026-01-16 14:42:43 +03:00
Daniah Ayad Al-sultani
56e2c0ffaa attendence login/logout has been implemented 2026-01-15 22:35:10 +03:00
Mohammed Al-Samarraie
3b3ed5e640 1111 2026-01-13 15:14:30 +03:00
Mohammed Al-Samarraie
7cbf65e6c1 Merge remote-tracking branch 'origin/master' 2026-01-13 15:09:41 +03:00
Mohammed Al-Samarraie
cefd2397fe 1111 2026-01-13 15:06:59 +03:00
74 changed files with 3102 additions and 626 deletions

26
.vscode/launch.json vendored Normal file
View 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"
}
]
}

View File

@@ -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:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../network/api_client.dart';
import '../../data/datasources/auth_remote_data_source.dart';
import '../../data/datasources/user_local_data_source.dart';
import '../../data/datasources/vacation_remote_data_source.dart';
import '../../data/datasources/advance_remote_data_source.dart';
import '../../data/repositories/auth_repository_impl.dart';
import '../../data/repositories/vacation_repository_impl.dart';
import '../../data/repositories/advance_repository_impl.dart';
import '../../domain/repositories/auth_repository.dart';
import '../../domain/repositories/vacation_repository.dart';
import '../../domain/repositories/advance_repository.dart';
import '../../domain/usecases/login_usecase.dart';
import '../../domain/usecases/attendance_login_usecase.dart';
import '../../domain/usecases/attendance_logout_usecase.dart';
import '../../domain/usecases/create_vacation_usecase.dart';
import '../../domain/usecases/get_vacation_types_usecase.dart';
import '../../domain/usecases/get_vacations_usecase.dart';
import '../../domain/usecases/create_advance_usecase.dart';
import '../../domain/usecases/get_advances_usecase.dart';
import '../../presentation/blocs/login/login_bloc.dart';
final sl = GetIt.instance;
@@ -34,16 +51,50 @@ Future<void> initializeDependencies() async {
// Repositories
sl.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(
remoteDataSource: sl(),
localDataSource: sl(),
),
() => AuthRepositoryImpl(remoteDataSource: sl(), localDataSource: sl()),
);
// Use cases
sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
// Blocs will be registered here
// Example:
// sl.registerFactory(() => LoginBloc(loginUseCase: sl()));
// Blocs
sl.registerFactory(() => LoginBloc(loginUseCase: sl()));
//Attendence
sl.registerLazySingleton<AttendanceRemoteDataSource>(
() => AttendanceRemoteDataSourceImpl(apiClient: sl()),
);
sl.registerLazySingleton<AttendanceRepository>(
() => AttendanceRepositoryImpl(remoteDataSource: sl()),
);
sl.registerLazySingleton(() => AttendanceLoginUsecase(repository: sl()));
sl.registerLazySingleton(() => AttendanceLogoutUseCase(repository: sl()));
// Vacation
sl.registerLazySingleton<VacationRemoteDataSource>(
() => VacationRemoteDataSourceImpl(apiClient: sl()),
);
sl.registerLazySingleton<VacationRepository>(
() => VacationRepositoryImpl(remoteDataSource: sl()),
);
sl.registerLazySingleton(() => CreateVacationUseCase(repository: sl()));
sl.registerLazySingleton(() => GetVacationTypesUseCase(repository: sl()));
sl.registerLazySingleton(() => GetVacationsUseCase(repository: sl()));
// Advance
sl.registerLazySingleton<AdvanceRemoteDataSource>(
() => AdvanceRemoteDataSourceImpl(apiClient: sl()),
);
sl.registerLazySingleton<AdvanceRepository>(
() => AdvanceRepositoryImpl(remoteDataSource: sl()),
);
sl.registerLazySingleton(() => CreateAdvanceUseCase(repository: sl()));
sl.registerLazySingleton(() => GetAdvancesUseCase(repository: sl()));
}

View File

@@ -1,6 +1,6 @@
import 'dart:async';
import '../models/leave_request.dart';
import '../models/advance_request.dart';
import '../../models/leave_request.dart';
import '../../models/advance_request.dart';
class RequestService {
// Singleton implementation

View 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: 'خطأ غير متوقع');
}
}
}

View 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: 'خطأ غير متوقع');
}
}
}

View File

@@ -16,10 +16,7 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
@override
Future<LoginResponseDto> login(LoginDto dto) async {
try {
final response = await apiClient.post(
'/Auth/login',
data: dto.toJson(),
);
final response = await apiClient.post('/Auth/login', data: dto.toJson());
if (response.statusCode == 200 || response.statusCode == 201) {
final responseData = response.data;
@@ -47,7 +44,8 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
} else if (e.response?.statusCode == 500) {
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
} else if (e.response != null) {
final message = e.response?.data?['message'] ??
final message =
e.response?.data?['message'] ??
e.response?.data?['error'] ??
'فشل تسجيل الدخول';

View File

@@ -4,11 +4,14 @@ abstract class UserLocalDataSource {
Future<void> cacheUserToken(String token);
Future<String?> getCachedUserToken();
Future<void> clearCache();
Future<void> cacheEmployeeId(String id);
Future<String?> getCachedEmployeeId();
}
class UserLocalDataSourceImpl implements UserLocalDataSource {
final SharedPreferences sharedPreferences;
static const String _tokenKey = 'user_token';
static const String _employeeIdKey = 'employee_id';
UserLocalDataSourceImpl({required this.sharedPreferences});
@@ -25,5 +28,16 @@ class UserLocalDataSourceImpl implements UserLocalDataSource {
@override
Future<void> clearCache() async {
await sharedPreferences.remove(_tokenKey);
await sharedPreferences.remove(_employeeIdKey);
}
@override
Future<void> cacheEmployeeId(String id) async {
await sharedPreferences.setString(_employeeIdKey, id);
}
@override
Future<String?> getCachedEmployeeId() async {
return sharedPreferences.getString(_employeeIdKey);
}
}

View 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: 'خطأ غير متوقع');
}
}
}

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

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

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

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

View File

@@ -57,13 +57,15 @@ class LoginDataDto {
return LoginDataDto(
token: json['token'],
id: json['id'],
employeeId: json['employeeId'],
employeeId:
json['employeeId'] ?? json['EmployeeId'] ?? json['employee_id'],
username: json['username'],
fullName: json['fullName'],
role: json['role'],
email: json['email'],
phoneNumber: json['phoneNumber'],
permissions: json['permissions'] != null
permissions:
json['permissions'] != null
? List<String>.from(json['permissions'])
: null,
);

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

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

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

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

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

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

View File

@@ -4,7 +4,6 @@ import '../../core/error/failures.dart';
import '../datasources/auth_remote_data_source.dart';
import '../datasources/user_local_data_source.dart';
import '../dto/login_dto.dart';
import '../dto/login_response_dto.dart';
import '../../domain/models/login_request.dart';
import '../../domain/models/login_response_model.dart';
import '../../domain/repositories/auth_repository.dart';
@@ -19,7 +18,9 @@ class AuthRepositoryImpl implements AuthRepository {
});
@override
Future<Either<Failure, LoginResponseModel>> login(LoginRequest request) async {
Future<Either<Failure, LoginResponseModel>> login(
LoginRequest request,
) async {
try {
final dto = LoginDto(
phoneNumber: request.phoneNumber,
@@ -27,18 +28,26 @@ class AuthRepositoryImpl implements AuthRepository {
);
final responseDto = await remoteDataSource.login(dto);
print("LOGIN RESPONSE DATA: ${responseDto.toJson()}"); // Debugging Log
// Cache the token locally
if (responseDto.data?.token != null) {
await localDataSource.cacheUserToken(responseDto.data!.token!);
}
if (responseDto.data?.employeeId != null) {
print("AUTH_REPO: Caching EmployeeId: ${responseDto.data!.employeeId}");
await localDataSource.cacheEmployeeId(responseDto.data!.employeeId!);
} else {
print("AUTH_REPO: EmployeeId is NULL in response!");
}
// Convert DTO to Model
final responseModel = LoginResponseModel(
statusCode: responseDto.statusCode,
isSuccess: responseDto.isSuccess,
message: responseDto.message,
data: responseDto.data != null
data:
responseDto.data != null
? LoginDataModel(
token: responseDto.data!.token,
id: responseDto.data!.id,

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

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

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

View File

@@ -0,0 +1,8 @@
import 'dart:io';
class AttendanceLoginRequest {
final String employeeId;
final File faceImage;
AttendanceLoginRequest({required this.employeeId, required this.faceImage});
}

View File

@@ -0,0 +1,8 @@
import 'dart:io';
class AttendanceLogoutRequest {
final String employeeId;
final File faceImage;
AttendanceLogoutRequest({required this.employeeId, required this.faceImage});
}

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

View File

@@ -2,8 +2,5 @@ class LoginRequest {
final String phoneNumber;
final String password;
LoginRequest({
required this.phoneNumber,
required this.password,
});
LoginRequest({required this.phoneNumber, required this.password});
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'core/di/injection_container.dart';
import 'screens/splash_screen.dart';
import 'presentation/screens/splash_screen.dart';
void main() async {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();

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

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

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

View File

@@ -1,9 +1,15 @@
import 'package:coda_project/screens/face_screen.dart';
import 'package:coda_project/screens/notifications_screen.dart';
import 'package:coda_project/screens/user_settings_screen.dart';
import 'package:coda_project/presentation/screens/face_screen.dart';
import 'package:coda_project/presentation/screens/notifications_screen.dart';
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../widgets/settings_bar.dart';
import '../../core/di/injection_container.dart';
import '../../domain/models/attendance_login_request.dart';
import '../../domain/models/attendance_logout_request.dart';
import '../../domain/usecases/attendance_login_usecase.dart';
import '../../domain/usecases/attendance_logout_usecase.dart';
import '../../data/datasources/user_local_data_source.dart';
class AttendanceScreen extends StatelessWidget {
const AttendanceScreen({super.key});
@@ -138,12 +144,40 @@ class AttendanceScreen extends StatelessWidget {
child: _FingerButton(
icon: "assets/images/faceLogin.svg",
label: "تسجيل الدخول",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => OvalCameraCapturePage(isLogin: true),
onTap: () async {
final employeeId =
await sl<UserLocalDataSource>().getCachedEmployeeId();
print("ATTENDANCE_SCREEN: Retrieved EmployeeId: $employeeId");
if (employeeId == null) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
),
);
}
return;
}
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(_) => OvalCameraCapturePage(
isLogin: true,
onCapture: (imageFile) async {
final loginUseCase =
sl<AttendanceLoginUsecase>();
await loginUseCase(
AttendanceLoginRequest(
employeeId: employeeId,
faceImage: imageFile,
),
);
},
),
),
);
}
},
),
),
@@ -178,12 +212,39 @@ class AttendanceScreen extends StatelessWidget {
child: _FingerButton(
icon: "assets/images/faceLogout.svg",
label: "تسجيل خروج",
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => OvalCameraCapturePage(isLogin: false),
onTap: () async {
final employeeId =
await sl<UserLocalDataSource>().getCachedEmployeeId();
if (employeeId == null) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
),
);
}
return;
}
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(_) => OvalCameraCapturePage(
isLogin: false,
onCapture: (imageFile) async {
final logoutUseCase =
sl<AttendanceLogoutUseCase>();
await logoutUseCase(
AttendanceLogoutRequest(
employeeId: employeeId,
faceImage: imageFile,
),
);
},
),
),
);
}
},
),
),

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

View File

@@ -2,10 +2,18 @@ import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'dart:async';
import 'dart:io';
import '../../core/error/exceptions.dart';
class OvalCameraCapturePage extends StatefulWidget {
final bool isLogin;
const OvalCameraCapturePage({super.key, this.isLogin = true});
final Future<void> Function(File image) onCapture;
const OvalCameraCapturePage({
super.key,
this.isLogin = true,
required this.onCapture,
});
@override
State<OvalCameraCapturePage> createState() => _OvalCameraCapturePageState();
@@ -16,6 +24,7 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
bool _isCameraInitialized = false;
String? _errorMessage;
bool _isSuccess = false;
bool _isLoading = false;
Timer? _timer;
@override
@@ -26,6 +35,11 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
Future<void> _initializeCamera() async {
try {
setState(() {
_errorMessage = null;
_isCameraInitialized = false;
});
// Dispose existing controller if any
await _cameraController?.dispose();
_cameraController = null;
@@ -35,8 +49,9 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
// Check if cameras list is available
if (cameras.isEmpty) {
if (!mounted) return;
setState(() {
_errorMessage = "لا توجد كاميرات متاحة";
_errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز";
_isCameraInitialized = false;
});
return;
@@ -53,8 +68,9 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
if (cameras.isNotEmpty) {
selectedCamera = cameras.first;
} else {
if (!mounted) return;
setState(() {
_errorMessage = "لا توجد كاميرات متاحة";
_errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز";
_isCameraInitialized = false;
});
return;
@@ -77,28 +93,140 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
_errorMessage = null;
});
_timer = Timer(const Duration(seconds: 3), () {
_startScan();
} on CameraException catch (e) {
if (!mounted) return;
String errorMessage;
switch (e.code) {
case 'CameraAccessDenied':
errorMessage = "تم رفض الوصول إلى الكاميرا. يرجى السماح بالوصول في الإعدادات";
break;
case 'CameraAccessDeniedWithoutPrompt':
errorMessage = "تم رفض الوصول إلى الكاميرا. يرجى السماح بالوصول في الإعدادات";
break;
case 'CameraAccessRestricted':
errorMessage = "الوصول إلى الكاميرا مقيد";
break;
case 'AudioAccessDenied':
errorMessage = "تم رفض الوصول إلى الميكروفون";
break;
default:
errorMessage = "خطأ في تهيئة الكاميرا: ${e.description ?? 'خطأ غير معروف'}";
}
setState(() {
_errorMessage = errorMessage;
_isCameraInitialized = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_errorMessage = "حدث خطأ غير متوقع أثناء تهيئة الكاميرا";
_isCameraInitialized = false;
});
print("Error initializing camera: $e");
}
}
Future<void> _startScan() async {
try {
setState(() {
_isLoading = true;
_errorMessage = null;
});
// Simulate scanning delay
await Future.delayed(const Duration(seconds: 2));
if (!mounted ||
_cameraController == null ||
!_cameraController!.value.isInitialized) {
if (mounted) {
setState(() {
_isLoading = false;
_errorMessage = "الكاميرا غير مهيأة. يرجى المحاولة مرة أخرى";
});
}
return;
}
final xFile = await _cameraController!.takePicture();
final file = File(xFile.path);
// Check if file exists and is readable
if (!await file.exists()) {
if (mounted) {
setState(() {
_isLoading = false;
_errorMessage = "فشل حفظ الصورة. يرجى المحاولة مرة أخرى";
});
}
return;
}
// Call the onCapture callback which may throw exceptions
await widget.onCapture(file);
if (mounted) {
setState(() {
_isSuccess = true;
_isLoading = false;
});
// Auto-close after 2 seconds
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
Navigator.of(context).pop();
Navigator.of(context).pop(true);
}
});
}
});
} catch (e) {
if (!mounted) return;
} on ServerException catch (e) {
if (mounted) {
setState(() {
_errorMessage = "خطأ في تهيئة الكاميرا: $e";
_isCameraInitialized = false;
_isLoading = false;
_errorMessage = e.message;
});
print("Error initializing camera: $e");
}
} 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");
}
}
@@ -115,7 +243,8 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
backgroundColor: Color(0xff000000),
body:
_errorMessage != null
// Show error screen only if camera failed to initialize
_errorMessage != null && !_isCameraInitialized
? Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
@@ -138,6 +267,9 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
textAlign: TextAlign.center,
),
SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _initializeCamera,
style: ElevatedButton.styleFrom(
@@ -150,6 +282,25 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
),
child: Text("إعادة المحاولة"),
),
SizedBox(width: 16),
OutlinedButton(
onPressed: () {
if (mounted) {
Navigator.of(context).pop(false);
}
},
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: BorderSide(color: Colors.white70),
padding: EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
),
child: Text("إلغاء"),
),
],
),
],
),
),
@@ -200,9 +351,13 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
? (widget.isLogin
? "تم تسجيل دخولك بنجاح"
: "تم تسجيل خروجك بنجاح")
: (widget.isLogin
: _isLoading
? (widget.isLogin
? "يتم تسجيل الدخول ..."
: "يتم تسجيل الخروج ..."),
: "يتم تسجيل الخروج ...")
: (widget.isLogin
? "جاهز للتقاط الصورة"
: "جاهز للتقاط الصورة"),
style: const TextStyle(
color: Colors.white,
fontSize: 18,
@@ -227,6 +382,81 @@ class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
),
),
// Error overlay (shown when error occurs during scanning)
if (_errorMessage != null && _isCameraInitialized)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.7),
child: Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
SizedBox(height: 16),
Text(
_errorMessage!,
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
setState(() {
_errorMessage = null;
_isLoading = false;
});
_startScan();
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xffE8001A),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
),
child: Text("إعادة المحاولة"),
),
SizedBox(width: 16),
OutlinedButton(
onPressed: () {
if (mounted) {
Navigator.of(context).pop(false);
}
},
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: BorderSide(color: Colors.white70),
padding: EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
),
child: Text("إلغاء"),
),
],
),
],
),
),
),
),
),
// // Capture button
// Positioned(
// bottom: 60,

View File

@@ -1,5 +1,5 @@
import 'package:coda_project/screens/notifications_screen.dart';
import 'package:coda_project/screens/user_settings_screen.dart';
import 'package:coda_project/presentation/screens/notifications_screen.dart';
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
import 'package:flutter/material.dart';
import '../widgets/finance_summary_card.dart';
import '../widgets/work_day_card.dart';

View File

@@ -1,15 +1,23 @@
import 'package:coda_project/screens/notifications_screen.dart';
import 'package:coda_project/screens/user_settings_screen.dart';
import 'package:coda_project/presentation/screens/notifications_screen.dart';
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../widgets/settings_bar.dart';
import '../screens/request_leave_screen.dart';
import '../screens/request_advance_scrren.dart';
import '../models/leave_request.dart';
import '../models/advance_request.dart';
import '../services/request_service.dart';
import 'request_leave_screen.dart';
import 'request_advance_scrren.dart';
import '../../models/leave_request.dart';
import '../../models/advance_request.dart';
import '../../core/services/request_service.dart';
import '../../core/di/injection_container.dart';
import '../../domain/usecases/get_vacations_usecase.dart';
import '../../domain/usecases/get_advances_usecase.dart';
import '../../domain/models/vacations_list_response_model.dart';
import '../../domain/models/vacation_response_model.dart';
import '../../domain/models/advances_list_response_model.dart';
import '../../domain/models/advance_request_model.dart';
import '../../core/error/failures.dart';
class HolidayScreen extends StatefulWidget {
final void Function(bool isScrollingDown)? onScrollEvent;
@@ -24,8 +32,12 @@ class _HolidayScreenState extends State<HolidayScreen> {
int activeTab = 0;
final RequestService _requestService = RequestService();
final GetVacationsUseCase _getVacationsUseCase = sl<GetVacationsUseCase>();
final GetAdvancesUseCase _getAdvancesUseCase = sl<GetAdvancesUseCase>();
List<LeaveRequest> _leaveRequests = [];
List<AdvanceRequest> _advanceRequests = [];
bool _isLoadingVacations = false;
bool _isLoadingAdvances = false;
final ScrollController _scrollController = ScrollController();
bool _showActions = true;
@@ -62,11 +74,18 @@ class _HolidayScreenState extends State<HolidayScreen> {
}
void _initializeData() async {
_leaveRequests = await _requestService.getLeaveRequests();
_advanceRequests = await _requestService.getAdvanceRequests();
// Load from API
_loadVacationsFromAPI();
_loadAdvancesFromAPI();
// Also listen to local changes (for newly created requests)
_requestService.leaveRequestsStream.listen((requests) {
if (mounted) setState(() => _leaveRequests = requests);
if (mounted) {
setState(() {
// Merge with API data if needed
_leaveRequests = requests;
});
}
});
_requestService.advanceRequestsStream.listen((requests) {
@@ -74,6 +93,140 @@ class _HolidayScreenState extends State<HolidayScreen> {
});
}
Future<void> _loadVacationsFromAPI() async {
setState(() {
_isLoadingVacations = true;
});
final result = await _getVacationsUseCase();
result.fold(
(failure) {
if (mounted) {
setState(() {
_isLoadingVacations = false;
});
// Load from local service as fallback
_requestService.getLeaveRequests().then((requests) {
if (mounted) {
setState(() {
_leaveRequests = requests;
});
}
});
}
},
(response) {
if (mounted && response.data != null) {
setState(() {
_leaveRequests = response.data!.items
.map((vacation) => _convertVacationToLeaveRequest(vacation))
.toList();
_isLoadingVacations = false;
});
}
},
);
}
LeaveRequest _convertVacationToLeaveRequest(VacationDataModel vacation) {
// Convert state (0=waiting, 1=approved, 2=denied) to status string
String status = "waiting";
if (vacation.state == 1) {
status = "approved";
} else if (vacation.state == 2) {
status = "denied";
}
// Convert type to Arabic name
String leaveTypeName = _getArabicVacationTypeName(vacation.type);
// Check if it's timed leave (same day but different times)
bool isTimedLeave = vacation.startDate.year == vacation.endDate.year &&
vacation.startDate.month == vacation.endDate.month &&
vacation.startDate.day == vacation.endDate.day &&
vacation.startDate.hour != vacation.endDate.hour;
return LeaveRequest(
id: vacation.id,
leaveType: leaveTypeName,
isTimedLeave: isTimedLeave,
fromDate: vacation.startDate,
toDate: vacation.endDate,
fromTime: TimeOfDay.fromDateTime(vacation.startDate),
toTime: TimeOfDay.fromDateTime(vacation.endDate),
reason: vacation.reason,
requestDate: vacation.createdAt ?? vacation.startDate,
status: status,
);
}
String _getArabicVacationTypeName(int type) {
switch (type) {
case 1:
return 'أجازة زمنية';
case 2:
return 'إجازة مرضية';
case 3:
return 'إجازة مدفوعة';
case 4:
return 'إجازة غير مدفوعة';
default:
return 'إجازة';
}
}
Future<void> _loadAdvancesFromAPI() async {
setState(() {
_isLoadingAdvances = true;
});
final result = await _getAdvancesUseCase();
result.fold(
(failure) {
if (mounted) {
setState(() {
_isLoadingAdvances = false;
});
// Load from local service as fallback
_requestService.getAdvanceRequests().then((requests) {
if (mounted) {
setState(() {
_advanceRequests = requests;
});
}
});
}
},
(response) {
if (mounted && response.data != null) {
setState(() {
_advanceRequests = response.data!.items
.map((advance) => _convertAdvanceToAdvanceRequest(advance))
.toList();
_isLoadingAdvances = false;
});
}
},
);
}
AdvanceRequest _convertAdvanceToAdvanceRequest(AdvanceDataModel advance) {
// Convert state (0=waiting, 1=approved, 2=denied) to status string
String status = "waiting";
if (advance.state == 1) {
status = "approved";
} else if (advance.state == 2) {
status = "denied";
}
return AdvanceRequest(
id: advance.id,
amount: advance.amount,
reason: advance.reason,
status: status,
);
}
@override
Widget build(BuildContext context) {
return Stack(
@@ -259,6 +412,19 @@ class _HolidayScreenState extends State<HolidayScreen> {
// ----------------------------------------------------------------
Widget _buildLeaveRequestsSliver() {
if (_isLoadingVacations) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(40.0),
child: Center(
child: CircularProgressIndicator(
color: Color(0xFF8EFDC2),
),
),
),
);
}
if (_leaveRequests.isEmpty) {
return SliverToBoxAdapter(
child: Padding(
@@ -287,6 +453,19 @@ class _HolidayScreenState extends State<HolidayScreen> {
}
Widget _buildAdvanceRequestsSliver() {
if (_isLoadingAdvances) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(40.0),
child: Center(
child: CircularProgressIndicator(
color: Color(0xFF8EFDC2),
),
),
),
);
}
if (_advanceRequests.isEmpty) {
return SliverToBoxAdapter(
child: Padding(

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import '../widgets/app_background.dart';
import '../widgets/floatingnavbar.dart';
import '../screens/attendence_screen.dart';
import '../screens/finance_screen.dart';
import '../screens/holiday_screen.dart';
import '../widgets/FloatingNavBar.dart';
import 'attendence_screen.dart';
import 'finance_screen.dart';
import 'holiday_screen.dart';
class MainPage extends StatefulWidget {
const MainPage({super.key});
@@ -15,15 +15,12 @@ class MainPage extends StatefulWidget {
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.sizeOf(context).height;
// final screenHeight = MediaQuery.sizeOf(context).height;
return Scaffold(
body: Stack(
children: [
/// BACKGROUND
const AppBackground(child: SizedBox()),

View File

@@ -1,5 +1,5 @@
import 'dart:async';
import 'package:coda_project/screens/auth_screen.dart';
import 'package:coda_project/presentation/screens/auth_screen.dart';
import 'package:flutter/material.dart';
import '../widgets/onboarding_page.dart';
import '../widgets/onboarding_button.dart';

View File

@@ -3,8 +3,13 @@ import 'package:flutter/material.dart';
import '../widgets/app_background.dart';
import '../widgets/settings_bar.dart';
import '../widgets/onboarding_button.dart';
import '../models/advance_request.dart';
import '../services/request_service.dart';
import '../../models/advance_request.dart';
import '../../core/services/request_service.dart';
import '../../core/di/injection_container.dart';
import '../../data/datasources/user_local_data_source.dart';
import '../../domain/usecases/create_advance_usecase.dart';
import '../../domain/models/advance_request_model.dart';
import '../../core/error/failures.dart';
class RequestAdvanceScreen extends StatefulWidget {
const RequestAdvanceScreen({super.key});
@@ -23,16 +28,30 @@ class _RequestAdvanceScreenState extends State<RequestAdvanceScreen> {
// Use the singleton instance
final RequestService _requestService = RequestService();
// Use case
final CreateAdvanceUseCase _createAdvanceUseCase = sl<CreateAdvanceUseCase>();
String _getFailureMessage(Failure failure) {
if (failure is ServerFailure) {
return failure.message;
} else if (failure is NetworkFailure) {
return failure.message;
}
return 'حدث خطأ غير متوقع';
}
// Method to save the advance request
Future<void> _saveAdvanceRequest() async {
if (amountController.text.isEmpty || reasonController.text.isEmpty) {
// Show an error message if fields are empty
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('الرجاء إدخال جميع الحقول'),
backgroundColor: Colors.red,
),
);
}
return;
}
@@ -40,28 +59,74 @@ class _RequestAdvanceScreenState extends State<RequestAdvanceScreen> {
final amount = double.tryParse(amountController.text);
if (amount == null || amount <= 0) {
// Show an error message if amount is invalid
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('الرجاء إدخال مبلغ صحيح'),
backgroundColor: Colors.red,
),
);
}
return;
}
// Create a new advance request with default status "waiting"
// Get employee ID
final employeeId = await sl<UserLocalDataSource>().getCachedEmployeeId();
if (employeeId == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
backgroundColor: Colors.red,
),
);
}
return;
}
// Show loading indicator
if (mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(child: CircularProgressIndicator()),
);
}
try {
// Create advance request model
final advanceRequestModel = AdvanceRequestModel(
employeeId: employeeId,
date: DateTime.now(),
amount: amount,
reason: reasonController.text,
);
final result = await _createAdvanceUseCase(advanceRequestModel);
if (mounted) {
Navigator.pop(context); // Close loading dialog
result.fold(
(failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_getFailureMessage(failure)),
backgroundColor: Colors.red,
),
);
},
(response) {
// Also save locally for UI display
final advanceRequest = AdvanceRequest(
id: DateTime.now().millisecondsSinceEpoch.toString(),
id: response.data?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
amount: amount,
reason: reasonController.text,
status: "waiting", // Default status
);
_requestService.addAdvanceRequest(advanceRequest);
try {
// Save the advance request
await _requestService.addAdvanceRequest(advanceRequest);
// Show a success message
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('تم إرسال طلب السلفة بنجاح'),
@@ -71,16 +136,21 @@ class _RequestAdvanceScreenState extends State<RequestAdvanceScreen> {
// Navigate back to the previous screen
Navigator.pop(context);
},
);
}
} catch (e) {
// Show an error message if something went wrong
if (mounted) {
Navigator.pop(context); // Close loading dialog
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('حدث خطأ: $e'),
content: Text('حدث خطأ غير متوقع: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {

View File

@@ -3,8 +3,15 @@ import 'package:flutter_svg/flutter_svg.dart';
import '../widgets/app_background.dart';
import '../widgets/settings_bar.dart';
import '../widgets/onboarding_button.dart';
import '../models/leave_request.dart';
import '../services/request_service.dart';
import '../../models/leave_request.dart';
import '../../core/services/request_service.dart';
import '../../core/di/injection_container.dart';
import '../../data/datasources/user_local_data_source.dart';
import '../../domain/usecases/create_vacation_usecase.dart';
import '../../domain/usecases/get_vacation_types_usecase.dart';
import '../../domain/models/vacation_request.dart';
import '../../domain/models/vacation_type_model.dart';
import '../../core/error/failures.dart';
class RequestLeaveScreen extends StatefulWidget {
const RequestLeaveScreen({super.key});
@@ -33,6 +40,14 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
// Use the singleton instance
final RequestService _requestService = RequestService();
// Use cases
final CreateVacationUseCase _createVacationUseCase = sl<CreateVacationUseCase>();
final GetVacationTypesUseCase _getVacationTypesUseCase = sl<GetVacationTypesUseCase>();
// Vacation types from API
List<VacationTypeModel> _vacationTypes = [];
int? _selectedVacationTypeValue; // Store selected type value instead of string
/// PICK DATE
Future<void> pickDate(bool isFrom) async {
DateTime initial = isFrom ? fromDate! : toDate!;
@@ -79,23 +94,177 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
}
}
@override
void initState() {
super.initState();
_loadVacationTypes();
}
Future<void> _loadVacationTypes() async {
final result = await _getVacationTypesUseCase();
result.fold(
(failure) {
// Silently fail - user can still submit with default types
print('Failed to load vacation types: ${_getFailureMessage(failure)}');
},
(response) {
if (mounted) {
setState(() {
_vacationTypes = response.data;
// Set default to SickLeave (value: 2) if available
if (_vacationTypes.isNotEmpty) {
final sickLeave = _vacationTypes.firstWhere(
(type) => type.value == 2,
orElse: () => _vacationTypes.first,
);
_selectedVacationTypeValue = sickLeave.value;
leaveType = _getArabicName(sickLeave.name);
}
});
}
},
);
}
String _getArabicName(String apiName) {
// Map API names to Arabic display names
switch (apiName) {
case 'TimeOff':
return 'أجازة زمنية';
case 'SickLeave':
return 'إجازة مرضية';
case 'PaidLeave':
return 'إجازة مدفوعة';
case 'UnpaidLeave':
return 'إجازة غير مدفوعة';
default:
return apiName;
}
}
int _getVacationTypeValue() {
// Use selected value if available, otherwise fallback to mapping
if (_selectedVacationTypeValue != null) {
return _selectedVacationTypeValue!;
}
// Fallback: Map display names to API type values
if (leaveType.contains("مرضية") || leaveType == "SickLeave") {
return 2; // SickLeave
} else if (leaveType.contains("مدفوعة") && !leaveType.contains("غير")) {
return 3; // PaidLeave
} else if (leaveType.contains("غير مدفوعة") || leaveType == "UnpaidLeave") {
return 4; // UnpaidLeave
} else if (leaveType.contains("زمنية") || leaveType == "TimeOff") {
return 1; // TimeOff
}
return 1; // Default to TimeOff
}
String _getFailureMessage(Failure failure) {
if (failure is ServerFailure) {
return failure.message;
} else if (failure is NetworkFailure) {
return failure.message;
}
return 'حدث خطأ غير متوقع';
}
// Method to save the leave request
Future<void> _saveLeaveRequest() async {
if (reasonController.text.isEmpty) {
// Show an error message if reason is empty
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('الرجاء إدخال السبب'),
backgroundColor: Colors.red,
),
);
}
return;
}
// Create a new leave request with default status "waiting"
// Get employee ID
final employeeId = await sl<UserLocalDataSource>().getCachedEmployeeId();
if (employeeId == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
backgroundColor: Colors.red,
),
);
}
return;
}
// Prepare dates - if timed leave, use same date with time differences
DateTime finalStartDate = fromDate!;
DateTime finalEndDate = toDate!;
if (isTimedLeave) {
// For timed leave, use the same date but with different times
finalStartDate = DateTime(
fromDate!.year,
fromDate!.month,
fromDate!.day,
fromTime!.hour,
fromTime!.minute,
);
finalEndDate = DateTime(
fromDate!.year,
fromDate!.month,
fromDate!.day,
toTime!.hour,
toTime!.minute,
);
} else {
// For regular leave, use dates at midnight
finalStartDate = DateTime(fromDate!.year, fromDate!.month, fromDate!.day);
finalEndDate = DateTime(toDate!.year, toDate!.month, toDate!.day);
}
// Get vacation type value
final typeValue = _getVacationTypeValue();
// Show loading indicator
if (mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(child: CircularProgressIndicator()),
);
}
try {
// Create vacation request
final vacationRequest = VacationRequest(
employeeId: employeeId,
startDate: finalStartDate,
endDate: finalEndDate,
reason: reasonController.text,
type: typeValue,
);
final result = await _createVacationUseCase(vacationRequest);
if (mounted) {
Navigator.pop(context); // Close loading dialog
result.fold(
(failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_getFailureMessage(failure)),
backgroundColor: Colors.red,
),
);
},
(response) {
// Also save locally for UI display
final leaveRequest = LeaveRequest(
id: DateTime.now().millisecondsSinceEpoch.toString(),
leaveType: leaveType, // Use the current leaveType value
id: response.data?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
leaveType: leaveType,
isTimedLeave: isTimedLeave,
fromDate: fromDate!,
toDate: toDate!,
@@ -105,12 +274,9 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
requestDate: DateTime.now(),
status: "waiting", // Default status
);
_requestService.addLeaveRequest(leaveRequest);
try {
// Save the leave request
await _requestService.addLeaveRequest(leaveRequest);
// Show a success message
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('تم إرسال طلب الأجازة بنجاح'),
@@ -120,12 +286,20 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
// Navigate back to the previous screen
Navigator.pop(context);
} catch (e) {
// Show an error message if something went wrong
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('حدث خطأ: $e'), backgroundColor: Colors.red),
},
);
}
} catch (e) {
if (mounted) {
Navigator.pop(context); // Close loading dialog
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('حدث خطأ غير متوقع: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
@@ -216,8 +390,15 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
],
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: leaveType,
child: _vacationTypes.isEmpty
? const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
)
: DropdownButton<int>(
value: _selectedVacationTypeValue,
icon: const Icon(
Icons.keyboard_arrow_down_rounded,
color: Colors.black,
@@ -229,54 +410,30 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
),
isExpanded: true,
onChanged: (value) {
if (value != null) {
setState(() {
leaveType = value!;
// Set toggle based on selected value
isTimedLeave = value == "أجازة زمنية";
_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: [
DropdownMenuItem(
value: "إجازة مرضية ",
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("إجازة مرضية "),
child: Text(arabicName),
),
),
),
DropdownMenuItem(
value: "إجازة مدفوعة",
child: Directionality(
textDirection: TextDirection.rtl,
child: Align(
alignment: Alignment.centerRight,
child: Text("إجازة مدفوعة"),
),
),
),
DropdownMenuItem(
value: "إجازة غير مدفوعة",
child: Directionality(
textDirection: TextDirection.rtl,
child: Align(
alignment: Alignment.centerRight,
child: Text("إجازة غير مدفوعة"),
),
),
),
DropdownMenuItem(
value: "أجازة زمنية",
child: Directionality(
textDirection: TextDirection.rtl,
child: Align(
alignment: Alignment.centerRight,
child: Text("أجازة زمنية"),
),
),
),
],
);
}).toList(),
),
),
),
@@ -293,9 +450,12 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
onTap: () {
setState(() {
isTimedLeave = !isTimedLeave;
// Set leave type to "أجازة زمنية" when toggle is ON
// Set leave type to TimeOff (value: 1) when toggle is ON
if (isTimedLeave) {
leaveType = "أجازة زمنية";
final timeOffType = _vacationTypes
.firstWhere((t) => t.value == 1, orElse: () => _vacationTypes.first);
_selectedVacationTypeValue = timeOffType.value;
leaveType = _getArabicName(timeOffType.name);
}
});
},

View File

@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'onboarding_screen.dart';
import 'main_screen.dart';
import '../../core/di/injection_container.dart';
import '../../data/datasources/user_local_data_source.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@@ -14,13 +17,31 @@ class _SplashScreenState extends State<SplashScreen> {
void initState() {
super.initState();
FlutterNativeSplash.remove();
_checkTokenAndNavigate();
}
Future.delayed(const Duration(seconds: 2), () {
Future<void> _checkTokenAndNavigate() async {
// Wait for splash screen display
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
// Check if token exists in cache
final token = await sl<UserLocalDataSource>().getCachedUserToken();
if (token != null && token.isNotEmpty) {
// Token exists, navigate directly to MainPage
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => OnboardingScreen()),
MaterialPageRoute(builder: (_) => const MainPage()),
);
});
} else {
// No token, navigate to OnboardingScreen
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const OnboardingScreen()),
);
}
}
@override

View File

@@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../widgets/app_background.dart';
import '../widgets/settings_bar.dart';
import '../screens/about_screen.dart';
import '../screens/auth_screen.dart';
import 'about_screen.dart';
import 'auth_screen.dart';
import '../widgets/change_password_modal.dart';
import '../../core/di/injection_container.dart';
import '../../data/datasources/user_local_data_source.dart';
class UserSettingsScreen extends StatefulWidget {
const UserSettingsScreen({super.key});
@@ -227,13 +228,18 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
_settingsRow(
label: "تسجيل خروج",
icon: "assets/images/logout2.svg",
onTap: () {
Navigator.push(
onTap: () async {
await sl<UserLocalDataSource>()
.clearCache();
if (context.mounted) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (_) => const AuthScreen(),
),
(route) => false,
);
}
},
),
],

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

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import '../widgets/onboarding_button.dart';
import 'onboarding_button.dart';
class ChangePasswordModal extends StatefulWidget {
const ChangePasswordModal({super.key});

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../widgets/app_background.dart';
import 'app_background.dart';
class LoginAnimationScreen extends StatefulWidget {
final bool isLogin;

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../widgets/gradient_line.dart';
import '../widgets/status_circle.dart';
import 'gradient_line.dart';
import 'status_circle.dart';
class WorkDayCard extends StatelessWidget {
const WorkDayCard({super.key});

View File

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

View File

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

View File

@@ -29,10 +29,18 @@ packages:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.12.0"
version: "2.13.0"
bloc:
dependency: transitive
description:
name: bloc
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
url: "https://pub.dev"
source: hosted
version: "8.1.4"
boolean_selector:
dependency: transitive
description:
@@ -181,10 +189,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.3"
ffi:
dependency: transitive
description:
@@ -206,6 +214,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
url: "https://pub.dev"
source: hosted
version: "8.1.6"
flutter_launcher_icons:
dependency: "direct dev"
description:
@@ -273,7 +289,7 @@ packages:
source: hosted
version: "0.15.6"
http:
dependency: "direct main"
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
@@ -308,10 +324,10 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev"
source: hosted
version: "10.0.8"
version: "10.0.9"
leak_tracker_flutter_testing:
dependency: transitive
description:
@@ -368,6 +384,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
@@ -440,6 +464,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.3"
provider:
dependency: transitive
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
shared_preferences:
dependency: "direct main"
description:
@@ -609,10 +641,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "14.3.1"
version: "15.0.0"
web:
dependency: transitive
description:

View File

@@ -17,6 +17,7 @@ dependencies:
dartz: ^0.10.1
equatable: ^2.0.5
shared_preferences: ^2.2.2
flutter_bloc: ^8.1.6
dev_dependencies:
flutter_test: