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 login({ required String employeeId, required File faceImage, bool localAuth = false, double? latitude, double? longitude, }); Future logout({ required String employeeId, required File faceImage, bool localAuth = false, double? latitude, double? longitude, }); Future> getAttendanceRecords({ required String employeeId, }); Future> getExtraHours({required String employeeId}); Future> getRewards({required String employeeId}); Future> getPunishments({required String employeeId}); Future getLastRecord({required String employeeId}); Future hasActiveLogin({required String employeeId}); Future calculateSalary({ required String employeeId, required int month, required int year, }); } class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource { final ApiClient apiClient; AttendanceRemoteDataSourceImpl({required this.apiClient}); @override Future 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) { 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 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) { 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> 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) { 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 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) { 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 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> 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) { 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> 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) { 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> 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) { 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 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) { 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) { 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: 'خطأ في الانترنيت يرجى المحاولة لاحقا'); } } }