461 lines
15 KiB
Dart
461 lines
15 KiB
Dart
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';
|
|
import '../dto/attendance_record_dto.dart';
|
|
import '../dto/overtime_dto.dart';
|
|
import '../dto/reward_dto.dart';
|
|
import '../dto/punishment_dto.dart';
|
|
import '../dto/salary_response_dto.dart';
|
|
|
|
abstract class AttendanceRemoteDataSource {
|
|
Future<AttendanceResponseDto> login({
|
|
required String employeeId,
|
|
required File faceImage,
|
|
bool localAuth = false,
|
|
double? latitude,
|
|
double? longitude,
|
|
});
|
|
|
|
Future<AttendanceResponseDto> logout({
|
|
required String employeeId,
|
|
required File faceImage,
|
|
bool localAuth = false,
|
|
double? latitude,
|
|
double? longitude,
|
|
});
|
|
|
|
Future<List<AttendanceRecordDto>> getAttendanceRecords({
|
|
required String employeeId,
|
|
});
|
|
Future<List<OvertimeDto>> getExtraHours({required String employeeId});
|
|
Future<List<RewardDto>> getRewards({required String employeeId});
|
|
Future<List<PunishmentDto>> getPunishments({required String employeeId});
|
|
Future<AttendanceRecordDto?> getLastRecord({required String employeeId});
|
|
Future<bool> hasActiveLogin({required String employeeId});
|
|
Future<SalaryResponseDto> calculateSalary({
|
|
required String employeeId,
|
|
required int month,
|
|
required int year,
|
|
});
|
|
}
|
|
|
|
class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|
final ApiClient apiClient;
|
|
|
|
AttendanceRemoteDataSourceImpl({required this.apiClient});
|
|
@override
|
|
Future<AttendanceResponseDto> login({
|
|
required String employeeId,
|
|
required File faceImage,
|
|
bool localAuth = false,
|
|
double? latitude,
|
|
double? longitude,
|
|
}) async {
|
|
try {
|
|
final formData = FormData.fromMap({
|
|
'EmployeeId': employeeId,
|
|
'FaceImage': await MultipartFile.fromFile(faceImage.path),
|
|
'IsAuth': localAuth.toString(),
|
|
'Domain': 'hrm.go.iq',
|
|
if (latitude != null) 'Latitude': latitude,
|
|
if (longitude != null) 'Longitude': longitude,
|
|
});
|
|
|
|
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,
|
|
bool localAuth = false,
|
|
double? latitude,
|
|
double? longitude,
|
|
}) async {
|
|
try {
|
|
final formData = FormData.fromMap({
|
|
'EmployeeId': employeeId,
|
|
'FaceImage': await MultipartFile.fromFile(faceImage.path),
|
|
'IsAuth': localAuth.toString(),
|
|
if (latitude != null) 'Latitude': latitude,
|
|
if (longitude != null) 'Longitude': longitude,
|
|
});
|
|
|
|
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) {
|
|
throw ServerException(message: 'خطأ غير متوقع');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<List<AttendanceRecordDto>> getAttendanceRecords({
|
|
required String employeeId,
|
|
}) async {
|
|
try {
|
|
final response = await apiClient.get(
|
|
'/Attendance',
|
|
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
|
);
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
final data = response.data;
|
|
if (data is Map<String, dynamic>) {
|
|
final items = (data['data'] ?? data['Data'])?['items'] as List? ?? [];
|
|
return items.map((e) => AttendanceRecordDto.fromJson(e)).toList();
|
|
}
|
|
return [];
|
|
} else {
|
|
throw ServerException(
|
|
message: 'فشل في جلب البيانات',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} on DioException catch (e) {
|
|
_handleDioError(e, 'فشل في جلب البيانات');
|
|
rethrow; // Should not reach here due to _handleDioError throwing
|
|
} catch (e) {
|
|
if (e is ServerException || e is NetworkException) rethrow;
|
|
throw ServerException(message: 'خطأ غير متوقع');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<AttendanceRecordDto?> getLastRecord({
|
|
required String employeeId,
|
|
}) async {
|
|
try {
|
|
final response = await apiClient.get(
|
|
'/Attendance',
|
|
queryParameters: {
|
|
'IsDeleted': false,
|
|
'EmployeeId': employeeId,
|
|
'PageNumber': 1,
|
|
'PageSize': 1,
|
|
'SortBy': 'CreatedAt',
|
|
'SortDescending': true,
|
|
},
|
|
);
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
final data = response.data;
|
|
if (data is Map<String, dynamic>) {
|
|
final items = (data['data'] ?? data['Data'])?['items'] as List? ?? [];
|
|
if (items.isNotEmpty) {
|
|
return AttendanceRecordDto.fromJson(items.first);
|
|
}
|
|
}
|
|
return null;
|
|
} else {
|
|
throw ServerException(
|
|
message: 'فشل في جلب آخر سجل',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} on DioException catch (e) {
|
|
_handleDioError(e, 'فشل في جلب آخر سجل');
|
|
rethrow;
|
|
} catch (e) {
|
|
if (e is ServerException || e is NetworkException) rethrow;
|
|
throw ServerException(message: 'خطأ غير متوقع');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> hasActiveLogin({required String employeeId}) async {
|
|
try {
|
|
final last = await getLastRecord(employeeId: employeeId);
|
|
if (last == null) return false;
|
|
|
|
// The API enforces a DAILY check ("Employee already logged in today")
|
|
// So we check: has a login TODAY, regardless of logout status
|
|
if (last.login != null) {
|
|
final now = DateTime.now();
|
|
final loginDate = last.login!;
|
|
if (loginDate.year == now.year &&
|
|
loginDate.month == now.month &&
|
|
loginDate.day == now.day) {
|
|
// Logged in today — if no logout, definitely active
|
|
// If logout exists, they already completed today's cycle
|
|
return last.logout == null;
|
|
}
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
// If the check fails, let the user try — the API will reject if needed
|
|
print('hasActiveLogin check failed: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<List<OvertimeDto>> getExtraHours({required String employeeId}) async {
|
|
try {
|
|
final response = await apiClient.get(
|
|
'/ExtraHours',
|
|
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
|
);
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
final responseData = response.data;
|
|
|
|
if (responseData is Map<String, dynamic>) {
|
|
return OvertimeListResponseDto.fromJson(responseData).items;
|
|
} else {
|
|
throw ServerException(
|
|
message: 'استجابة غير صحيحة من الخادم',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} else {
|
|
throw ServerException(
|
|
message: 'فشل في جلب البيانات',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} on DioException catch (e) {
|
|
_handleDioError(e, 'فشل في جلب البيانات');
|
|
rethrow;
|
|
} catch (e) {
|
|
if (e is ServerException || e is NetworkException) rethrow;
|
|
throw ServerException(message: 'خطأ غير متوقع');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<List<RewardDto>> getRewards({required String employeeId}) async {
|
|
try {
|
|
final response = await apiClient.get(
|
|
'/Reward',
|
|
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
|
);
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
final responseData = response.data;
|
|
if (responseData is Map<String, dynamic>) {
|
|
return RewardListResponseDto.fromJson(responseData).items;
|
|
}
|
|
return [];
|
|
} else {
|
|
throw ServerException(
|
|
message: 'فشل في جلب المكافآت',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} on DioException catch (e) {
|
|
_handleDioError(e, 'فشل في جلب المكافآت');
|
|
rethrow;
|
|
} catch (e) {
|
|
if (e is ServerException || e is NetworkException) rethrow;
|
|
throw ServerException(message: 'خطأ غير متوقع');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<List<PunishmentDto>> getPunishments({
|
|
required String employeeId,
|
|
}) async {
|
|
try {
|
|
final response = await apiClient.get(
|
|
'/Punishment',
|
|
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
|
);
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
final responseData = response.data;
|
|
if (responseData is Map<String, dynamic>) {
|
|
return PunishmentListResponseDto.fromJson(responseData).items;
|
|
}
|
|
return [];
|
|
} else {
|
|
throw ServerException(
|
|
message: 'فشل في جلب البيانات',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} on DioException catch (e) {
|
|
_handleDioError(e, 'فشل في جلب البيانات');
|
|
rethrow;
|
|
} catch (e) {
|
|
if (e is ServerException || e is NetworkException) rethrow;
|
|
throw ServerException(message: 'خطأ غير متوقع');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<SalaryResponseDto> calculateSalary({
|
|
required String employeeId,
|
|
required int month,
|
|
required int year,
|
|
}) async {
|
|
try {
|
|
final response = await apiClient.get(
|
|
'/SalaryRecord/calculate',
|
|
queryParameters: {
|
|
'EmployeeId': employeeId,
|
|
'Month': month,
|
|
'Year': year,
|
|
},
|
|
);
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
final responseData = response.data;
|
|
|
|
if (responseData is Map<String, dynamic>) {
|
|
return SalaryResponseDto.fromJson(responseData);
|
|
} else if (responseData is num) {
|
|
return SalaryResponseDto(
|
|
isSuccess: true,
|
|
message: 'Success',
|
|
data: SalaryDataDto(netAmount: responseData.toDouble()),
|
|
);
|
|
} else if (responseData is String &&
|
|
double.tryParse(responseData) != null) {
|
|
return SalaryResponseDto(
|
|
isSuccess: true,
|
|
message: 'Success',
|
|
data: SalaryDataDto(netAmount: double.parse(responseData)),
|
|
);
|
|
} else {
|
|
throw ServerException(
|
|
message: 'استجابة غير صحيحة من الخادم',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} else {
|
|
throw ServerException(
|
|
message: 'فشل في حساب الراتب',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} on DioException catch (e) {
|
|
_handleDioError(e, 'فشل في حساب الراتب');
|
|
rethrow;
|
|
} catch (e) {
|
|
if (e is ServerException || e is NetworkException) rethrow;
|
|
throw ServerException(message: 'خطأ غير متوقع');
|
|
}
|
|
}
|
|
|
|
void _handleDioError(DioException e, String defaultMessage) {
|
|
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 data = e.response?.data;
|
|
String? message;
|
|
|
|
if (data is Map<String, dynamic>) {
|
|
message = data['message']?.toString() ?? data['error']?.toString();
|
|
} else if (data is String) {
|
|
message = data;
|
|
}
|
|
|
|
throw ServerException(
|
|
message: message ?? defaultMessage,
|
|
statusCode: e.response?.statusCode,
|
|
);
|
|
} else {
|
|
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
|
}
|
|
}
|
|
}
|