import 'dart:convert'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; // Helper Services import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/api_endpoints.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/utils/encryption_helper.dart'; // Models import 'package:on_field_work/model/dashboard/project_progress_model.dart'; import 'package:on_field_work/model/dashboard/dashboard_tasks_model.dart'; import 'package:on_field_work/model/dashboard/dashboard_teams_model.dart'; import 'package:on_field_work/model/document/document_filter_model.dart'; import 'package:on_field_work/model/document/documents_list_model.dart'; import 'package:on_field_work/model/document/master_document_tags.dart'; import 'package:on_field_work/model/document/master_document_type_model.dart'; import 'package:on_field_work/model/document/document_details_model.dart'; import 'package:on_field_work/model/document/document_version_model.dart'; import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart'; import 'package:on_field_work/model/tenant/tenant_services_model.dart'; import 'package:on_field_work/model/dailyTaskPlanning/daily_task_model.dart'; import 'package:on_field_work/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart'; import 'package:on_field_work/model/all_organization_model.dart'; import 'package:on_field_work/model/dashboard/pending_expenses_model.dart'; import 'package:on_field_work/model/dashboard/expense_type_report_model.dart'; import 'package:on_field_work/model/dashboard/monthly_expence_model.dart'; import 'package:on_field_work/model/finance/expense_category_model.dart'; import 'package:on_field_work/model/finance/currency_list_model.dart'; import 'package:on_field_work/model/finance/payment_payee_request_model.dart'; import 'package:on_field_work/model/finance/payment_request_list_model.dart'; import 'package:on_field_work/model/finance/payment_request_filter.dart'; import 'package:on_field_work/model/finance/payment_request_details_model.dart'; import 'package:on_field_work/model/finance/advance_payment_model.dart'; import 'package:on_field_work/model/service_project/service_projects_list_model.dart'; import 'package:on_field_work/model/service_project/service_projects_details_model.dart'; import 'package:on_field_work/model/service_project/job_list_model.dart'; import 'package:on_field_work/model/service_project/service_project_job_detail_model.dart'; import 'package:on_field_work/model/service_project/job_attendance_logs_model.dart'; import 'package:on_field_work/model/service_project/job_allocation_model.dart'; import 'package:on_field_work/model/service_project/service_project_branches_model.dart'; import 'package:on_field_work/model/service_project/job_status_response.dart'; import 'package:on_field_work/model/service_project/job_comments.dart'; import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/model/infra_project/infra_project_list.dart'; import 'package:on_field_work/model/infra_project/infra_project_details.dart'; import 'package:on_field_work/model/dashboard/collection_overview_model.dart'; import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart'; class ApiService { static const bool enableLogs = true; static const Duration extendedTimeout = Duration(seconds: 60); static void _log(String message, {LogLevel level = LogLevel.info}) { if (enableLogs) { logSafe(message, level: level); } } /// Utility to get the token, handle expiry, and refresh if needed. static Future _getToken() async { final token = LocalStorage.getJwtToken(); if (token == null) { _log("No JWT token found. Logging out...", level: LogLevel.error); await LocalStorage.logout(); return null; } try { final expirationDate = JwtDecoder.getExpirationDate(token); final difference = expirationDate.difference(DateTime.now()); if (JwtDecoder.isExpired(token) || difference.inMinutes < 2) { _log( "Token expired or near expiry (${difference.inSeconds}s). Attempting refresh...", level: LogLevel.warning, ); final refreshed = await AuthService.refreshToken(); if (refreshed) { return LocalStorage.getJwtToken(); } else { _log("Token refresh failed. Logging out.", level: LogLevel.error); await LocalStorage.logout(); return null; } } } catch (e) { _log("Token decoding error: $e", level: LogLevel.error); await LocalStorage.logout(); return null; } return token; } static Map _headers(String token) => { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }; // --- Centralized Response Parsing and Decryption --- /// Handles decryption, status code checks, and returns the 'data' payload or the full JSON. static dynamic _parseAndDecryptResponse(http.Response response, {String label = '', bool returnFullResponse = false}) { final body = response.body.trim(); _log("$label Encrypted Response (Status ${response.statusCode}): $body", level: LogLevel.debug); if (body.isEmpty) { _log("Empty response body for [$label]"); return null; } // Decrypt the Base64 string and auto-decode JSON final decryptedJson = decryptResponse(body); if (decryptedJson == null) { _log("Decryption failed for [$label]. Cannot parse response.", level: LogLevel.error); return null; } // Handle non-200 or failure scenarios if (response.statusCode != 200 || decryptedJson is! Map || decryptedJson['success'] != true) { final message = decryptedJson is Map ? decryptedJson['message'] ?? 'Unknown error' : 'Invalid decrypted response format.'; _log("API Error [$label] (Status ${response.statusCode}): $message", level: LogLevel.warning); return null; } _log( "$label Decrypted Data: ${decryptedJson['data'] ?? 'Success (No data field)'}"); return returnFullResponse ? decryptedJson : decryptedJson['data']; } // --- Generic Request Execution Layer --- /// Executes any HTTP request (GET, POST, PUT, DELETE, PATCH) safely with token management and retry logic. static Future _safeApiCall( String endpoint, { String method = 'GET', Map? queryParams, dynamic body, Map? additionalHeaders, Duration customTimeout = extendedTimeout, bool hasRetried = false, }) async { String? token = await _getToken(); if (token == null) { _log("Token is null. Cannot proceed with $method request.", level: LogLevel.error); return null; } final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint").replace( queryParameters: queryParams, ); final headers = { ..._headers(token), if (additionalHeaders != null) ...additionalHeaders, }; _log("Initiating $method request to $uri", level: LogLevel.debug); if (body != null) { _log("Request Body: ${body is String ? body : jsonEncode(body)}", level: LogLevel.debug); } try { http.Response response; final encodedBody = (body != null) ? jsonEncode(body) : null; switch (method) { case 'GET': response = await http.get(uri, headers: headers).timeout(customTimeout); break; case 'POST': response = await http .post(uri, headers: headers, body: encodedBody) .timeout(customTimeout); break; case 'PUT': response = await http .put(uri, headers: headers, body: encodedBody) .timeout(customTimeout); break; case 'DELETE': response = await http.delete(uri, headers: headers).timeout(customTimeout); break; case 'PATCH': response = await http .patch(uri, headers: headers, body: encodedBody) .timeout(customTimeout); break; default: throw UnsupportedError("Unsupported HTTP method: $method"); } _log("$method Response Status: ${response.statusCode}", level: LogLevel.debug); if (response.statusCode == 401 && !hasRetried) { _log("Unauthorized (401). Attempting token refresh and retry...", level: LogLevel.warning); if (await AuthService.refreshToken()) { return await _safeApiCall( endpoint, method: method, queryParams: queryParams, body: body, additionalHeaders: additionalHeaders, customTimeout: customTimeout, hasRetried: true, // Prevent infinite loop ); } _log("Token refresh failed on 401. Logging out user.", level: LogLevel.error); await LocalStorage.logout(); } return response; } catch (e, stack) { _log("HTTP $method Exception: $e\n$stack", level: LogLevel.error); return null; } } // --- Public API Methods (Utilizing Generic Call) --- /// ============================================ /// DASHBOARD /// ============================================ static Future getPurchaseInvoiceOverview({ String? projectId, }) async { final queryParams = {}; if (projectId != null && projectId.isNotEmpty) { queryParams['projectId'] = projectId; } final response = await _safeApiCall( ApiEndpoints.getPurchaseInvoiceOverview, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final parsedJson = _parseAndDecryptResponse( response, label: "PurchaseInvoiceOverview", returnFullResponse: true, ); if (parsedJson == null) return null; return PurchaseInvoiceOverviewResponse.fromJson(parsedJson); } static Future getCollectionOverview({ String? projectId, }) async { final queryParams = {}; if (projectId != null && projectId.isNotEmpty) { queryParams['projectId'] = projectId; } final response = await _safeApiCall( ApiEndpoints.getCollectionOverview, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final parsedJson = _parseAndDecryptResponse(response, label: "CollectionOverview", returnFullResponse: true); if (parsedJson == null) return null; return CollectionOverviewResponse.fromJson(parsedJson); } static Future getDashboardTasks( {required String projectId}) async { final response = await _safeApiCall( ApiEndpoints.getDashboardTasks, method: 'GET', queryParams: {'projectId': projectId}, ); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "DashboardTasks", returnFullResponse: true); if (jsonResponse == null) return null; return DashboardTasks.fromJson(jsonResponse); } static Future getDashboardTeams( {required String projectId}) async { final response = await _safeApiCall( ApiEndpoints.getDashboardTeams, method: 'GET', queryParams: {'projectId': projectId}, ); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "DashboardTeams", returnFullResponse: true); if (jsonResponse == null) return null; return DashboardTeams.fromJson(jsonResponse); } static Future?> getDashboardAttendanceOverview( String projectId, int days) async { if (projectId.isEmpty) throw ArgumentError('projectId must not be empty'); if (days <= 0) throw ArgumentError('days must be greater than 0'); final endpoint = "${ApiEndpoints.getDashboardAttendanceOverview}/$projectId"; final response = await _safeApiCall( endpoint, method: 'GET', queryParams: {'days': days.toString()}, ); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Dashboard Attendance Overview'); } static Future getProjectProgress({ required String projectId, required int days, DateTime? fromDate, }) async { final actualFromDate = fromDate ?? DateTime.now(); final queryParams = { "projectId": projectId, "days": days.toString(), "FromDate": DateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS").format(actualFromDate), }; final response = await _safeApiCall( ApiEndpoints.getDashboardProjectProgress, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final parsed = _parseAndDecryptResponse(response, label: "ProjectProgress", returnFullResponse: true); if (parsed == null) return null; return ProjectResponse.fromJson(parsed); } static Future getPendingExpensesApi({ required String projectId, }) async { final response = await _safeApiCall( ApiEndpoints.getPendingExpenses, method: 'GET', queryParams: {'projectId': projectId}, ); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Pending Expenses", returnFullResponse: true); if (jsonResponse == null) return null; return PendingExpensesResponse.fromJson(jsonResponse); } static Future getExpenseTypeReportApi({ required String projectId, required DateTime startDate, required DateTime endDate, }) async { final response = await _safeApiCall( ApiEndpoints.getExpenseTypeReport, method: 'GET', queryParams: { 'projectId': projectId, 'startDate': startDate.toIso8601String(), 'endDate': endDate.toIso8601String(), }, ); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Expense Category Report", returnFullResponse: true); if (jsonResponse == null) return null; return ExpenseTypeReportResponse.fromJson(jsonResponse); } static Future getDashboardMonthlyExpensesApi({ String? categoryId, int months = 12, }) async { final queryParams = { 'months': months.toString(), if (categoryId != null && categoryId.isNotEmpty) 'categoryId': categoryId, }; final response = await _safeApiCall( ApiEndpoints.getDashboardMonthlyExpenses, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Dashboard Monthly Expenses", returnFullResponse: true); if (jsonResponse == null) return null; return DashboardMonthlyExpenseResponse.fromJson(jsonResponse); } /// ============================================ /// INFRA PROJECT /// ============================================ static Future getInfraProjectsList({ int pageSize = 20, int pageNumber = 1, String searchString = "", }) async { final queryParams = { "pageSize": pageSize.toString(), "pageNumber": pageNumber.toString(), "searchString": searchString, }; final response = await _safeApiCall( ApiEndpoints.getInfraProjectsList, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final parsedJson = _parseAndDecryptResponse(response, label: "InfraProjectsList", returnFullResponse: true); if (parsedJson == null) return null; return ProjectsResponse.fromJson(parsedJson); } static Future getInfraProjectDetails({ required String projectId, }) async { final endpoint = "${ApiEndpoints.getInfraProjectDetail}/$projectId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final parsedJson = _parseAndDecryptResponse(response, label: "InfraProjectDetails", returnFullResponse: true); if (parsedJson == null) return null; return ProjectDetailsResponse.fromJson(parsedJson); } /// ============================================ /// SERVICE PROJECT /// ============================================ static Future getServiceProjectsListApi({ int pageNumber = 1, int pageSize = 20, }) async { final queryParams = { 'pageNumber': pageNumber.toString(), 'pageSize': pageSize.toString(), }; final response = await _safeApiCall( ApiEndpoints.getServiceProjectsList, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Service Project List", returnFullResponse: true); if (jsonResponse == null) return null; return ServiceProjectListModel.fromJson(jsonResponse); } static Future getServiceProjectDetailApi( String projectId) async { final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Service Project Detail", returnFullResponse: true); if (jsonResponse == null) return null; return ServiceProjectDetailModel.fromJson(jsonResponse); } static Future getServiceProjectJobListApi({ required String projectId, int pageNumber = 1, int pageSize = 20, bool isActive = true, bool isArchive = false, }) async { final queryParams = { 'projectId': projectId, 'pageNumber': pageNumber.toString(), 'pageSize': pageSize.toString(), 'isActive': isActive.toString(), if (isArchive) 'isArchive': 'true', }; final response = await _safeApiCall( ApiEndpoints.getServiceProjectJobList, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: isArchive ? "Archived Service Project Job List" : "Active Service Project Job List", returnFullResponse: true); if (jsonResponse == null) return null; return JobResponse.fromJson(jsonResponse); } static Future createServiceProjectJobApi({ required String title, required String description, required String projectId, required List> assignees, required DateTime startDate, required DateTime dueDate, required List> tags, required String? branchId, }) async { final body = { "title": title, "description": description, "projectId": projectId, "assignees": assignees, "startDate": startDate.toIso8601String(), "dueDate": dueDate.toIso8601String(), "tags": tags, "projectBranchId": branchId, }; final response = await _safeApiCall( ApiEndpoints.createServiceProjectJob, method: 'POST', body: body, ); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse( response, label: "Create Service Project Job", returnFullResponse: true, ); if (jsonResponse == null) return null; return jsonResponse['data']?['id']; } static Future getServiceProjectJobDetailApi( String jobId) async { final endpoint = "${ApiEndpoints.getServiceProjectJobDetail}/$jobId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Service Project Job Detail", returnFullResponse: true); if (jsonResponse == null) return null; return JobDetailsResponse.fromJson(jsonResponse); } static Future editServiceProjectJobApi({ required String jobId, required List> operations, }) async { final endpoint = "${ApiEndpoints.editServiceProjectJob}/$jobId"; // Using PATCH for partial update (JSON Patch format expected by server) final response = await _safeApiCall( endpoint, method: 'PATCH', body: operations, ); if (response == null) return false; // A PATCH request usually returns 200/204. Use parse to check for 'success' flag. final parsed = _parseAndDecryptResponse(response, label: "Edit Service Project Job", returnFullResponse: true); return parsed != null && parsed['success'] == true; } // Add this method back to ApiService: static Future?> getEmployees({ Map? queryParams, }) async { final response = await _safeApiCall( ApiEndpoints.getEmployeesWithoutPermission, method: 'GET', queryParams: queryParams, ); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Get Employees'); } static Future updateServiceProjectJobAttendance({ required Map payload, }) async { final response = await _safeApiCall( ApiEndpoints.serviceProjectUpateJobAttendance, method: 'POST', body: payload, ); if (response == null) return false; // Use parse to check for 'success' flag. final parsed = _parseAndDecryptResponse(response, label: "Update Job Attendance", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future getJobAttendanceLog({ required String attendanceId, }) async { final endpoint = "${ApiEndpoints.serviceProjectUpateJobAttendanceLog}/$attendanceId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final parsedJson = _parseAndDecryptResponse(response, label: "JobAttendanceLog", returnFullResponse: true); if (parsedJson == null) return null; return JobAttendanceResponse.fromJson(parsedJson); } static Future?> getServiceProjectAllocationList({ required String projectId, bool isActive = true, }) async { final queryParams = { 'projectId': projectId, 'isActive': isActive.toString(), }; final response = await _safeApiCall( ApiEndpoints.getServiceProjectUpateJobAllocationList, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final data = _parseAndDecryptResponse(response, label: "ServiceProjectAllocationList"); if (data is List) { return data.map((e) => ServiceProjectAllocation.fromJson(e)).toList(); } return null; } static Future manageServiceProjectAllocation({ required List> payload, }) async { final response = await _safeApiCall( ApiEndpoints.manageServiceProjectUpateJobAllocation, method: 'POST', body: payload, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Manage Allocation", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future?> getTeamRoles() async { final response = await _safeApiCall(ApiEndpoints.getTeamRoles, method: 'GET'); if (response == null) return null; final data = _parseAndDecryptResponse(response, label: "TeamRoles"); if (data is List) { return data .map((e) => TeamRole.fromJson(e as Map)) .toList(); } return null; } static Future getServiceProjectBranchesFull({ required String projectId, int pageNumber = 1, int pageSize = 20, String searchString = '', bool isActive = true, }) async { final queryParams = { 'pageNumber': pageNumber.toString(), 'pageSize': pageSize.toString(), 'searchString': searchString, 'isActive': isActive.toString(), }; final endpoint = "${ApiEndpoints.getServiceProjectBranches}/$projectId"; final response = await _safeApiCall( endpoint, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final parsedJson = _parseAndDecryptResponse( response, label: "ServiceProjectBranchesFull", returnFullResponse: true, ); if (parsedJson == null) return null; return ServiceProjectBranchesResponse.fromJson(parsedJson); } static Future?> getMasterJobStatus({ required String statusId, required String projectId, }) async { final queryParams = { 'statusId': statusId, 'projectId': projectId, }; final response = await _safeApiCall( ApiEndpoints.getMasterJobStatus, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final data = _parseAndDecryptResponse(response, label: "MasterJobStatus"); if (data is List) { return data.map((e) => JobStatus.fromJson(e)).toList(); } return null; } static Future addJobComment({ required String jobTicketId, required String comment, List> attachments = const [], }) async { final body = { "jobTicketId": jobTicketId, "comment": comment, "attachments": attachments, }; final response = await _safeApiCall( ApiEndpoints.addJobComment, method: 'POST', body: body, ); if (response == null) return false; // Check status 201 for success, or rely on decrypted 'success' flag final parsed = _parseAndDecryptResponse(response, label: "AddJobComment", returnFullResponse: true); return response.statusCode == 201 || (parsed != null && parsed['success'] == true); } static Future getJobCommentList({ required String jobTicketId, int pageNumber = 1, int pageSize = 20, }) async { final queryParams = { 'jobTicketId': jobTicketId, 'pageNumber': pageNumber.toString(), 'pageSize': pageSize.toString(), }; final response = await _safeApiCall( ApiEndpoints.getJobCommentList, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final parsedJson = _parseAndDecryptResponse(response, label: "JobCommentList", returnFullResponse: true); if (parsedJson == null) return null; return JobCommentResponse.fromJson(parsedJson); } /// ============================================ /// FINANCE & EXPENSE /// ============================================ static Future getExpensePaymentRequestPayeeApi() async { final response = await _safeApiCall( ApiEndpoints.getExpensePaymentRequestPayee, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Expense Payment Request Payee", returnFullResponse: true); if (jsonResponse == null) return null; return PaymentRequestPayeeResponse.fromJson(jsonResponse); } static Future getMasterExpenseCategoriesApi() async { final response = await _safeApiCall( ApiEndpoints.getMasterExpensesCategories, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Master Expense Categories", returnFullResponse: true); if (jsonResponse == null) return null; return ExpenseCategoryResponse.fromJson(jsonResponse); } static Future getMasterCurrenciesApi() async { final response = await _safeApiCall(ApiEndpoints.getMasterCurrencies, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Master Currencies", returnFullResponse: true); if (jsonResponse == null) return null; return CurrencyListResponse.fromJson(jsonResponse); } static Future createExpensePaymentRequestApi({ required String title, required String projectId, required String expenseCategoryId, required String currencyId, required String payee, required double amount, DateTime? dueDate, required String description, required bool isAdvancePayment, List> billAttachments = const [], }) async { final body = { "title": title, "projectId": projectId, "expenseCategoryId": expenseCategoryId, "currencyId": currencyId, "payee": payee, "amount": amount, "dueDate": dueDate?.toIso8601String(), "description": description, "isAdvancePayment": isAdvancePayment, "billAttachments": billAttachments, }; final response = await _safeApiCall( ApiEndpoints.createExpensePaymentRequest, method: 'POST', body: body, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Create Payment Request", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future getExpensePaymentRequestListApi({ bool isActive = true, int pageSize = 20, int pageNumber = 1, Map? filter, String searchString = '', }) async { final queryParams = { 'isActive': isActive.toString(), 'pageSize': pageSize.toString(), 'pageNumber': pageNumber.toString(), 'filter': jsonEncode(filter ?? { "projectIds": [], "statusIds": [], "createdByIds": [], "currencyIds": [], "expenseCategoryIds": [], "payees": [], "startDate": null, "endDate": null }), 'searchString': searchString, }; final response = await _safeApiCall( ApiEndpoints.getExpensePaymentRequestList, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Expense Payment Request List", returnFullResponse: true); if (jsonResponse == null) return null; return PaymentRequestResponse.fromJson(jsonResponse); } static Future getExpensePaymentRequestFilterApi() async { final response = await _safeApiCall( ApiEndpoints.getExpensePaymentRequestFilter, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Expense Payment Request Filter", returnFullResponse: true); if (jsonResponse == null) return null; return PaymentRequestFilter.fromJson(jsonResponse); } static Future getExpensePaymentRequestDetailApi( String paymentRequestId) async { final endpoint = "${ApiEndpoints.getExpensePaymentRequestDetails}/$paymentRequestId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Expense Payment Request Detail", returnFullResponse: true); if (jsonResponse == null) return null; return PaymentRequestDetail.fromJson(jsonResponse); } static Future updateExpensePaymentRequestStatusApi({ required String paymentRequestId, required String statusId, required String comment, String? paidTransactionId, String? paidById, DateTime? paidAt, double? baseAmount, double? taxAmount, String? tdsPercentage, }) async { final body = { "paymentRequestId": paymentRequestId, "statusId": statusId, "comment": comment, "paidTransactionId": paidTransactionId, "paidById": paidById, "paidAt": paidAt?.toIso8601String(), "baseAmount": baseAmount, "taxAmount": taxAmount, "tdsPercentage": tdsPercentage ?? "0", }; final response = await _safeApiCall( ApiEndpoints.updateExpensePaymentRequestStatus, method: 'POST', body: body, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Update PR Status", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future createExpenseForPRApi({ required String paymentModeId, required String location, required String gstNumber, required String statusId, required String paymentRequestId, required String comment, List> billAttachments = const [], }) async { final body = { "paymentModeId": paymentModeId, "location": location, "gstNumber": gstNumber, "statusId": statusId, "comment": comment, "paymentRequestId": paymentRequestId, "billAttachments": billAttachments, }; final response = await _safeApiCall( ApiEndpoints.createExpenseforPR, method: 'POST', body: body, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Create Expense for PR", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future editExpensePaymentRequestApi({ required String id, required String title, required String description, required String payee, required String currencyId, required double amount, required String dueDate, required String projectId, required String expenseCategoryId, required bool isAdvancePayment, List> billAttachments = const [], }) async { final endpoint = "${ApiEndpoints.getExpensePaymentRequestEdit}/$id"; final body = { "id": id, "title": title, "description": description, "payee": payee, "currencyId": currencyId, "amount": amount, "dueDate": dueDate, "projectId": projectId, "expenseCategoryId": expenseCategoryId, "isAdvancePayment": isAdvancePayment, "billAttachments": billAttachments, }; final response = await _safeApiCall( endpoint, method: 'PUT', body: body, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Edit Expense PR", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future> getAdvancePayments( String employeeId) async { final endpoint = "${ApiEndpoints.getAdvancePayments}/$employeeId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return []; final data = _parseAndDecryptResponse(response, label: 'Advance Payments'); if (data is List) { return data.map((e) => AdvancePayment.fromJson(e)).toList(); } return []; } static Future?> getMasterPaymentModes() async { final response = await _safeApiCall(ApiEndpoints.getMasterPaymentModes, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Master Payment Modes'); } static Future?> getMasterExpenseStatus() async { final response = await _safeApiCall(ApiEndpoints.getMasterExpenseStatus, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Master Expense Status'); } static Future?> getMasterExpenseTypes() async { final response = await _safeApiCall(ApiEndpoints.getMasterExpenseCategory, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Master Expense Categorys'); } static Future createExpenseApi({ required String projectId, required String expensesTypeId, required String paymentModeId, required String paidById, required DateTime transactionDate, required String transactionId, required String description, required String location, required String supplerName, required double amount, required int noOfPersons, required List> billAttachments, }) async { final payload = { "projectId": projectId, "expenseCategoryId": expensesTypeId, "paymentModeId": paymentModeId, "paidById": paidById, "transactionDate": transactionDate.toIso8601String(), "transactionId": transactionId, "description": description, "location": location, "supplerName": supplerName, "amount": amount, "noOfPersons": noOfPersons, "billAttachments": billAttachments, }; final response = await _safeApiCall( ApiEndpoints.createExpense, method: 'POST', body: payload, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Create Expense", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future?> getExpenseDetailsApi({ required String expenseId, }) async { final endpoint = "${ApiEndpoints.getExpenseDetails}/$expenseId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; // NOTE: This API currently does NOT return encrypted response based on original implementation. // Assuming the decrypted response returns the whole map containing 'data' final jsonResponse = _parseAndDecryptResponse(response, label: "Expense Details", returnFullResponse: true); return (jsonResponse != null && jsonResponse['data'] is Map) ? jsonResponse['data'] : null; } static Future updateExpenseStatusApi({ required String expenseId, required String statusId, String? comment, String? reimburseTransactionId, String? reimburseDate, String? reimbursedById, double? baseAmount, double? taxAmount, double? tdsPercent, double? netPayable, }) async { final payload = { "expenseId": expenseId, "statusId": statusId, if (comment != null) "comment": comment, if (reimburseTransactionId != null) "reimburseTransactionId": reimburseTransactionId, if (reimburseDate != null) "reimburseDate": reimburseDate, if (reimbursedById != null) "reimburseById": reimbursedById, if (baseAmount != null) "baseAmount": baseAmount, if (taxAmount != null) "taxAmount": taxAmount, if (tdsPercent != null) "tdsPercent": tdsPercent, if (netPayable != null) "netPayable": netPayable, }; final response = await _safeApiCall( ApiEndpoints.updateExpenseStatus, method: 'POST', body: payload, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Update Expense Status", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future editExpenseApi({ required String expenseId, required Map payload, }) async { final endpoint = "${ApiEndpoints.editExpense}/$expenseId"; final response = await _safeApiCall( endpoint, method: 'PUT', body: payload, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Edit Expense", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future deleteExpense(String expenseId) async { final endpoint = "${ApiEndpoints.deleteExpense}/$expenseId"; final response = await _safeApiCall(endpoint, method: 'DELETE'); if (response == null) return false; // DELETE requests often return 200 with a success body, or 204 (No Content). // Rely on parse function which checks for 200 and 'success: true' final parsed = _parseAndDecryptResponse(response, label: "Delete Expense", returnFullResponse: true); return response.statusCode == 204 || (parsed != null && parsed['success'] == true); } static Future?> getExpenseListApi({ String? filter, int pageSize = 20, int pageNumber = 1, }) async { final queryParams = { 'pageSize': pageSize.toString(), 'pageNumber': pageNumber.toString(), if (filter?.isNotEmpty ?? false) 'filter': filter!, }; final response = await _safeApiCall( ApiEndpoints.getExpenseList, method: 'GET', queryParams: queryParams, ); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Expense List", returnFullResponse: true); return jsonResponse is Map ? jsonResponse : null; } /// ============================================ /// DOCUMENT MANAGEMENT /// ============================================ static Future getDocumentFilters( String entityTypeId) async { final endpoint = "${ApiEndpoints.getDocumentFilter}/$entityTypeId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Document Filters", returnFullResponse: true); if (jsonResponse == null) return null; return DocumentFiltersResponse.fromJson(jsonResponse); } static Future getDocumentListApi({ required String entityTypeId, required String entityId, String filter = "", String searchString = "", bool isActive = true, int pageNumber = 1, int pageSize = 20, }) async { final endpoint = "${ApiEndpoints.getDocumentList}/$entityTypeId/entity/$entityId"; final queryParams = { "filter": filter, "searchString": searchString, "isActive": isActive.toString(), "pageNumber": pageNumber.toString(), "pageSize": pageSize.toString(), }; final response = await _safeApiCall(endpoint, method: 'GET', queryParams: queryParams); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Document List", returnFullResponse: true); if (jsonResponse == null) return null; return DocumentsResponse.fromJson(jsonResponse); } static Future getMasterDocumentTagsApi() async { final response = await _safeApiCall(ApiEndpoints.getMasterDocumentTags, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Master Document Tags", returnFullResponse: true); if (jsonResponse == null) return null; return TagResponse.fromJson(jsonResponse); } static Future uploadDocumentApi({ required String name, String? documentId, String? description, required String entityId, required String documentTypeId, required String fileName, required String base64Data, required String contentType, required int fileSize, String? fileDescription, bool isActive = true, List> tags = const [], }) async { final payload = { "name": name, "documentId": documentId ?? "", "description": description ?? "", "entityId": entityId, "documentTypeId": documentTypeId, "attachment": { "fileName": fileName, "base64Data": base64Data, "contentType": contentType, "fileSize": fileSize, "description": fileDescription ?? "", "isActive": isActive, }, "tags": tags.isNotEmpty ? tags : [ {"name": "default", "isActive": true} ], }; final response = await _safeApiCall( ApiEndpoints.uploadDocument, method: 'POST', body: payload, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Upload Document", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future getMasterDocumentTypesApi() async { final response = await _safeApiCall( ApiEndpoints.getMasterDocumentCategories, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Master Document Types", returnFullResponse: true); if (jsonResponse == null) return null; return DocumentTypesResponse.fromJson(jsonResponse); } static Future getDocumentTypesByCategoryApi( String documentCategoryId) async { final response = await _safeApiCall( ApiEndpoints.getDocumentTypesByCategory, method: 'GET', queryParams: {"documentCategoryId": documentCategoryId}, ); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Document Types by Category", returnFullResponse: true); if (jsonResponse == null) return null; return DocumentTypesResponse.fromJson(jsonResponse); } static Future getDocumentDetailsApi( String documentId) async { final endpoint = "${ApiEndpoints.getDocumentDetails}/$documentId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Document Details", returnFullResponse: true); if (jsonResponse == null) return null; return DocumentDetailsResponse.fromJson(jsonResponse); } static Future getDocumentVersionsApi({ required String parentAttachmentId, int pageNumber = 1, int pageSize = 20, }) async { final endpoint = "${ApiEndpoints.getDocumentVersions}/$parentAttachmentId"; final queryParams = { "pageNumber": pageNumber.toString(), "pageSize": pageSize.toString(), }; final response = await _safeApiCall(endpoint, method: 'GET', queryParams: queryParams); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Document Versions", returnFullResponse: true); if (jsonResponse == null) return null; return DocumentVersionsResponse.fromJson(jsonResponse); } static Future editDocumentApi({ required String id, required String name, required String documentId, String? description, List> tags = const [], Map? attachment, }) async { final endpoint = "${ApiEndpoints.editDocument}/$id"; final payload = { "id": id, "name": name, "documentId": documentId, "description": description ?? "", "tags": tags.isNotEmpty ? tags : [ {"name": "default", "isActive": true} ], "attachment": attachment, }; final response = await _safeApiCall(endpoint, method: 'PUT', body: payload); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Edit Document", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future deleteDocumentApi({ required String id, bool isActive = false, }) async { final endpoint = "${ApiEndpoints.deleteDocument}/$id"; final queryParams = {"isActive": isActive.toString()}; // Note: This endpoint is handled via DELETE with query params. final response = await _safeApiCall( endpoint, method: 'DELETE', queryParams: queryParams, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Delete Document", returnFullResponse: true); return response.statusCode == 200 && (parsed != null && parsed['success'] == true); } static Future getPresignedUrlApi(String versionId) async { final endpoint = "${ApiEndpoints.getDocumentVersion}/$versionId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Pre-Signed URL"); return jsonResponse is String ? jsonResponse : null; } static Future verifyDocumentApi({ required String id, bool isVerify = true, }) async { final endpoint = "${ApiEndpoints.verifyDocument}/$id"; final queryParams = {"isVerify": isVerify.toString()}; // Note: The original code used POST for this endpoint, retaining that. final response = await _safeApiCall( endpoint, method: 'POST', queryParams: queryParams, body: {}, // Must send a body for POST ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Verify Document", returnFullResponse: true); return parsed != null && parsed['success'] == true; } /// ============================================ /// EMPLOYEE & ORGANIZATION /// ============================================ static Future?> allEmployeesBasic({ bool allEmployee = true, }) async { final response = await _safeApiCall( ApiEndpoints.getEmployeesWithoutPermission, method: 'GET', queryParams: {'allEmployee': allEmployee.toString()}, ); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'All Employees Basic'); } static Future?> searchEmployeesBasic({ String? searchString, }) async { final queryParams = {}; if (searchString != null && searchString.isNotEmpty) { queryParams['searchString'] = searchString; } final response = await _safeApiCall( ApiEndpoints.getEmployeesWithoutPermission, method: 'GET', queryParams: queryParams, ); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Search Employees Basic'); } static Future?> getAllEmployeesByProject(String projectId, {String? organizationId}) async { if (projectId.isEmpty) throw ArgumentError('projectId must not be empty'); final endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId"; final queryParams = { if (organizationId != null && organizationId.isNotEmpty) "organizationId": organizationId }; final response = await _safeApiCall( endpoint, method: 'GET', queryParams: queryParams, ); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Employees by Project'); } static Future?> getEmployeesByProjectService( String projectId, { String? serviceId, String? organizationId, }) async { if (projectId.isEmpty) throw ArgumentError('projectId must not be empty'); final queryParams = {}; if (serviceId != null && serviceId.isNotEmpty) { queryParams['serviceId'] = serviceId; } if (organizationId != null && organizationId.isNotEmpty) { queryParams['organizationId'] = organizationId; } final endpoint = "${ApiEndpoints.getAllEmployeesByOrganization}/$projectId"; final response = await _safeApiCall( endpoint, method: 'GET', queryParams: queryParams, ); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Employees by Project Service'); } static Future?> getAllEmployees( {String? organizationId}) async { final queryParams = { if (organizationId != null && organizationId.isNotEmpty) "organizationId": organizationId }; final response = await _safeApiCall( ApiEndpoints.getAllEmployees, method: 'GET', queryParams: queryParams, ); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'All Employees'); } static Future?> getRoles() async { final response = await _safeApiCall(ApiEndpoints.getRoles, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Roles'); } static Future?> createEmployee({ String? id, required String firstName, required String lastName, required String phoneNumber, required String gender, required String jobRoleId, required String joiningDate, String? email, String? organizationId, bool? hasApplicationAccess, }) async { final body = { if (id != null) "id": id, "firstName": firstName, "lastName": lastName, "phoneNumber": phoneNumber, "gender": gender, "jobRoleId": jobRoleId, "joiningDate": joiningDate, if (email != null && email.isNotEmpty) "email": email, if (organizationId != null && organizationId.isNotEmpty) "organizationId": organizationId, if (hasApplicationAccess != null) "hasApplicationAccess": hasApplicationAccess, }; final response = await _safeApiCall( ApiEndpoints.createEmployee, method: 'POST', body: body, ); if (response == null) return null; final json = jsonDecode(response.body); // Non-encrypted body expected for this path? // Assuming we need to check the raw JSON status since the model is unclear return { "success": response.statusCode == 200 && (json['success'] == true), "data": json, }; } static Future?> getEmployeeDetails( String employeeId) async { final url = "${ApiEndpoints.getEmployeeInfo}/$employeeId"; final response = await _safeApiCall(url, method: 'GET'); if (response == null) return null; final data = _parseAndDecryptResponse(response, label: 'Employee Details'); return data is Map ? data : null; } static Future?> getAssignedProjects(String employeeId) async { if (employeeId.isEmpty) throw ArgumentError("employeeId must not be empty"); final endpoint = "${ApiEndpoints.getAssignedProjects}/$employeeId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final parsed = _parseAndDecryptResponse(response, label: "Assigned Projects"); return parsed is List ? parsed : null; } static Future assignProjects({ required String employeeId, required List> projects, }) async { if (employeeId.isEmpty) throw ArgumentError("employeeId must not be empty"); if (projects.isEmpty) _log("Warning: Projects list is empty in assignProjects."); final endpoint = "${ApiEndpoints.assignProjects}/$employeeId"; final response = await _safeApiCall( endpoint, method: 'POST', body: projects, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Assign Projects", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future?> getOrganizationHierarchyList( String employeeId) async { if (employeeId.isEmpty) throw ArgumentError('employeeId must not be empty'); final endpoint = "${ApiEndpoints.getOrganizationHierarchyList}/$employeeId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Organization Hierarchy List'); } static Future manageOrganizationHierarchy({ required String employeeId, required List> payload, }) async { if (employeeId.isEmpty) throw ArgumentError('employeeId must not be empty'); final endpoint = "${ApiEndpoints.manageOrganizationHierarchy}/$employeeId"; final response = await _safeApiCall( endpoint, method: 'POST', body: payload, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Manage Hierarchy", returnFullResponse: true); return parsed != null && parsed['success'] == true; } /// ============================================ /// PROJECT MANAGEMENT /// ============================================ static Future createProjectApi({ required String name, required String projectAddress, required String shortName, required String contactPerson, required DateTime startDate, required DateTime endDate, required String projectStatusId, }) async { final payload = { "name": name, "projectAddress": projectAddress, "shortName": shortName, "contactPerson": contactPerson, "startDate": startDate.toIso8601String(), "endDate": endDate.toIso8601String(), "projectStatusId": projectStatusId, }; final response = await _safeApiCall( ApiEndpoints.createProject, method: 'POST', body: payload, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Create Project", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future getAssignedOrganizations( String projectId) async { final endpoint = "${ApiEndpoints.getAssignedOrganizations}/$projectId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Assigned Organizations", returnFullResponse: true); if (jsonResponse == null) return null; return OrganizationListResponse.fromJson(jsonResponse); } static Future getAllOrganizations() async { final response = await _safeApiCall(ApiEndpoints.getAllOrganizations, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "All Organizations", returnFullResponse: true); if (jsonResponse == null) return null; return AllOrganizationListResponse.fromJson(jsonResponse); } static Future getAssignedServices( String projectId) async { final endpoint = "${ApiEndpoints.getAssignedServices}/$projectId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Assigned Services", returnFullResponse: true); if (jsonResponse == null) return null; return ServiceListResponse.fromJson(jsonResponse); } /// ============================================ /// DAILY TASK PLANNING /// ============================================ static Future getDailyTaskFilter( String projectId) async { final endpoint = "${ApiEndpoints.getDailyTaskProjectProgressFilter}/$projectId"; final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final jsonResponse = _parseAndDecryptResponse(response, label: "Daily Task Progress Filter", returnFullResponse: true); if (jsonResponse == null) return null; return DailyProgressReportFilterResponse.fromJson(jsonResponse); } static Future?> getDailyTasks( String projectId, { Map? filter, int pageNumber = 1, int pageSize = 20, }) async { final query = { "projectId": projectId, "pageNumber": pageNumber.toString(), "pageSize": pageSize.toString(), if (filter != null) "filter": jsonEncode(filter), }; final response = await _safeApiCall( ApiEndpoints.getDailyTask, method: 'GET', queryParams: query, ); if (response == null) return null; final parsed = _parseAndDecryptResponse(response, label: 'Daily Tasks'); if (parsed != null && parsed is Map && parsed['data'] is List) { return (parsed['data'] as List) .map((e) => TaskModel.fromJson(e)) .toList(); } return null; } static Future reportTask({ required String id, required int completedTask, required String comment, required List> checkList, List>? images, }) async { final body = { "id": id, "completedTask": completedTask, "comment": comment, "reportedDate": DateTime.now().toUtc().toIso8601String(), "checkList": checkList, if (images != null && images.isNotEmpty) "images": images, }; final response = await _safeApiCall( ApiEndpoints.reportTask, method: 'POST', body: body, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Report Task", returnFullResponse: true); if (parsed != null && parsed['success'] == true) { Get.back(); // Retaining Get.back() as per original logic return true; } return false; } static Future commentTask({ required String id, required String comment, List>? images, }) async { final body = { "taskAllocationId": id, "comment": comment, "commentDate": DateTime.now().toUtc().toIso8601String(), if (images != null && images.isNotEmpty) "images": images, }; final response = await _safeApiCall( ApiEndpoints.commentTask, method: 'POST', body: body, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Comment Task", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future?> getInfraDetails(String projectId, {String? serviceId}) async { String endpoint = "/project/infra-details/$projectId"; final queryParams = { if (serviceId != null && serviceId.isNotEmpty) 'serviceId': serviceId, }; final response = await _safeApiCall( endpoint, method: 'GET', queryParams: queryParams, ); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Infra Details', returnFullResponse: true) as Map?; } static Future?> getWorkItemsByWorkArea(String workAreaId, {String? serviceId}) async { String endpoint = "/project/tasks/$workAreaId"; final queryParams = { if (serviceId != null && serviceId.isNotEmpty) 'serviceId': serviceId, }; final response = await _safeApiCall( endpoint, method: 'GET', queryParams: queryParams, ); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Work Items', returnFullResponse: true) as Map?; } static Future assignDailyTask({ required String workItemId, required int plannedTask, required String description, required List taskTeam, DateTime? assignmentDate, String? organizationId, String? serviceId, }) async { final body = { "workItemId": workItemId, "plannedTask": plannedTask, "description": description, "taskTeam": taskTeam, "organizationId": organizationId, "serviceId": serviceId, "assignmentDate": (assignmentDate ?? DateTime.now()).toUtc().toIso8601String(), }; final response = await _safeApiCall( ApiEndpoints.assignDailyTask, method: 'POST', body: body, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Assign Daily Task", returnFullResponse: true); if (parsed != null && parsed['success'] == true) { Get.back(); // Retaining Get.back() as per original logic return true; } return false; } static Future?> getWorkStatus() async { final response = await _safeApiCall(ApiEndpoints.getWorkStatus, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Work Status', returnFullResponse: true) as Map?; } static Future?> getMasterWorkCategories() async { final response = await _safeApiCall(ApiEndpoints.getmasterWorkCategories, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Master Work Categories', returnFullResponse: true) as Map?; } static Future approveTask({ required String id, required String comment, required String workStatus, required int approvedTask, List>? images, }) async { final body = { "id": id, "workStatus": workStatus, "approvedTask": approvedTask, "comment": comment, if (images != null && images.isNotEmpty) "images": images, }; final response = await _safeApiCall( ApiEndpoints.approveReportAction, method: 'POST', body: body, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Approve Task", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future createTask({ required String parentTaskId, required int plannedTask, required String comment, required String workAreaId, required String activityId, DateTime? assignmentDate, required String categoryId, }) async { final body = [ { "parentTaskId": parentTaskId, "plannedWork": plannedTask, "comment": comment, "workAreaID": workAreaId, "activityID": activityId, "workCategoryId": categoryId, 'completedWork': 0, } ]; final response = await _safeApiCall( ApiEndpoints.assignTask, method: 'POST', body: body, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Create Task", returnFullResponse: true); if (parsed != null && parsed['success'] == true) { Get.back(); // Retaining Get.back() as per original logic return true; } return false; } /// ============================================ /// LOGGING & MENU /// ============================================ static Future postLogsApi(List> logs) async { // NOTE: This call retrieves the token directly and bypasses refresh logic // to avoid an infinite loop if logging itself fails. final token = await LocalStorage.getJwtToken(); if (token == null) { _log("No token available. Skipping logs post.", level: LogLevel.warning); return false; } final uri = Uri.parse("${ApiEndpoints.baseUrl}${ApiEndpoints.uploadLogs}"); final headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }; try { final response = await http .post(uri, headers: headers, body: jsonEncode(logs)) .timeout(ApiService.extendedTimeout); _log("Post logs response status: ${response.statusCode}"); // Decryption chain for API response final decryptedJson = decryptResponse(response.body); if (response.statusCode == 200 && decryptedJson is Map && decryptedJson['success'] == true) { _log("Logs posted successfully."); return true; } _log( "Failed to post logs: ${decryptedJson?['message'] ?? 'Unknown error'}", level: LogLevel.warning); } catch (e, stack) { _log("Exception during postLogsApi: $e\n$stack", level: LogLevel.error); } return false; } // ApiService.dart (around line 1400) static Future?> getMenuApi() async { final response = await _safeApiCall(ApiEndpoints.getDynamicMenu, method: 'GET'); if (response == null) return null; // Use the centralized parsing/decryption utility. // The server is clearly sending an encrypted response based on the log error. final jsonResponse = _parseAndDecryptResponse( response, label: "Dynamic Menu", returnFullResponse: true, ); // Return the full decrypted JSON map if parsing and 'success: true' check passed return jsonResponse is Map ? jsonResponse : null; } /// ============================================ /// ATTENDANCE /// ============================================ static Future?> getProjects() async { final response = await _safeApiCall(ApiEndpoints.getProjects, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Projects'); } static Future?> getGlobalProjects() async { final response = await _safeApiCall(ApiEndpoints.getGlobalProjects, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Global Projects'); } static Future?> getTodaysAttendance( String projectId, { String? organizationId, }) async { final query = { "projectId": projectId, if (organizationId != null) "organizationId": organizationId, }; final response = await _safeApiCall(ApiEndpoints.getTodaysAttendance, method: 'GET', queryParams: query); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Employees'); } static Future?> getAttendanceForDashboard( String projectId) async { final endpoint = ApiEndpoints.getAttendanceForDashboard.replaceFirst( ':projectId', projectId, ); final response = await _safeApiCall(endpoint, method: 'GET'); if (response == null) return null; final data = _parseAndDecryptResponse(response, label: 'Dashboard Attendance'); if (data is Map) { return [EmployeeModel.fromJson(data)]; } else if (data is List) { return data.map((e) => EmployeeModel.fromJson(e)).toList(); } return null; } static Future?> getRegularizationLogs( String projectId, { String? organizationId, }) async { final query = { "projectId": projectId, if (organizationId != null) "organizationId": organizationId, }; final response = await _safeApiCall(ApiEndpoints.getRegularizationLogs, method: 'GET', queryParams: query); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Regularization Logs'); } static Future?> getAttendanceLogs( String projectId, { DateTime? dateFrom, DateTime? dateTo, String? organizationId, }) async { final query = { "projectId": projectId, if (dateFrom != null) "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), if (organizationId != null) "organizationId": organizationId, }; final response = await _safeApiCall(ApiEndpoints.getAttendanceLogs, method: 'GET', queryParams: query); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Attendance Logs'); } static Future?> getAttendanceLogView(String id) async { final response = await _safeApiCall( "${ApiEndpoints.getAttendanceLogView}/$id", method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Log Details'); } static Future uploadAttendanceImage( String id, String employeeId, XFile? imageFile, double latitude, double longitude, { required String imageName, required String projectId, String comment = "", required int action, bool imageCapture = true, required String markTime, required String date, }) async { final body = { "id": id, "employeeId": employeeId, "projectId": projectId, "markTime": markTime, "comment": comment, "action": action, "date": date, if (imageCapture) "latitude": '$latitude', if (imageCapture) "longitude": '$longitude', }; if (imageCapture && imageFile != null) { try { final bytes = await imageFile.readAsBytes(); final fileSize = await imageFile.length(); final contentType = "image/${imageFile.path.split('.').last}"; body["image"] = { "fileName": imageName, "contentType": contentType, "fileSize": fileSize, "description": "Employee attendance photo", "base64Data": base64Encode(bytes), }; } catch (e) { _log("Image encoding error: $e", level: LogLevel.error); return false; } } final response = await _safeApiCall( ApiEndpoints.uploadAttendanceImage, method: 'POST', body: body, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Upload Attendance Image", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static String generateImageName(String employeeId, int count) { final now = DateTime.now(); final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now); final imageNumber = count.toString().padLeft(3, '0'); return "${employeeId}_${dateStr}_$imageNumber.jpg"; } /// ============================================ /// DIRECTORY /// ============================================ static Future?> getContactBucketList() async { final response = await _safeApiCall(ApiEndpoints.getDirectoryBucketList, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Contact Bucket List', returnFullResponse: true) as Map?; } static Future?> getDirectoryData( {required bool isActive}) async { final queryParams = {"active": isActive.toString()}; final response = await _safeApiCall( ApiEndpoints.getDirectoryContacts, method: 'GET', queryParams: queryParams, ); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Directory Data'); } static Future?> getContactTagList() async { final response = await _safeApiCall(ApiEndpoints.getDirectoryContactTags, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Contact Tag List', returnFullResponse: true) as Map?; } static Future?> getContactCategoryList() async { final response = await _safeApiCall( ApiEndpoints.getDirectoryContactCategory, method: 'GET'); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Contact Category List', returnFullResponse: true) as Map?; } static Future> getOrganizationList() async { final response = await _safeApiCall(ApiEndpoints.getDirectoryOrganization, method: 'GET'); if (response == null) return []; final body = _parseAndDecryptResponse(response, label: 'Organization List'); if (body is List) { return List.from(body); } return []; } static Future createContact(Map payload) async { final response = await _safeApiCall(ApiEndpoints.createContact, method: 'POST', body: payload); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Create Contact", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future updateContact( String contactId, Map payload) async { final endpoint = "${ApiEndpoints.updateContact}/$contactId"; final response = await _safeApiCall(endpoint, method: 'PUT', body: payload); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Update Contact", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future deleteDirectoryContact(String contactId) async { // Note: The original implementation uses a DELETE request with an 'active=false' query param for soft delete. final endpoint = "${ApiEndpoints.updateContact}/$contactId/"; final queryParams = {'active': 'false'}; final response = await _safeApiCall( endpoint, method: 'DELETE', queryParams: queryParams, ); if (response == null) return false; // Assuming successful deletion is 200/204 return response.statusCode == 200 || response.statusCode == 204; } static Future restoreDirectoryContact(String contactId) async { // Note: The original implementation uses a DELETE request with an 'active=true' query param for restore. final endpoint = "${ApiEndpoints.updateContact}/$contactId/"; final queryParams = {'active': 'true'}; final response = await _safeApiCall( endpoint, method: 'DELETE', queryParams: queryParams, ); if (response == null) return false; // Assuming successful restore is 200/204 return response.statusCode == 200 || response.statusCode == 204; } static Future?> getDirectoryComments( String contactId, { bool active = true, }) async { final endpoint = "${ApiEndpoints.getDirectoryNotes}/$contactId"; final queryParams = {'active': active.toString()}; final response = await _safeApiCall(endpoint, method: 'GET', queryParams: queryParams); if (response == null) return null; final data = _parseAndDecryptResponse(response, label: 'Directory Comments'); return data is List ? data : null; } static Future restoreContactComment( String commentId, bool isActive, ) async { final endpoint = "${ApiEndpoints.updateDirectoryNotes}/$commentId"; final queryParams = {'active': isActive.toString()}; // Note: The original implementation used DELETE for status update final response = await _safeApiCall( endpoint, method: 'DELETE', queryParams: queryParams, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Restore/Delete Comment", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future updateContactComment( String commentId, String note, String contactId) async { final payload = { "id": commentId, "contactId": contactId, "note": note, }; final endpoint = "${ApiEndpoints.updateDirectoryNotes}/$commentId"; final headers = {"comment-id": commentId}; // Per original logic final response = await _safeApiCall( endpoint, method: 'PUT', body: payload, additionalHeaders: headers, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Update Comment", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future addContactComment(String note, String contactId) async { final payload = { "note": note, "contactId": contactId, }; final response = await _safeApiCall( ApiEndpoints.updateDirectoryNotes, method: 'POST', body: payload, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Add Comment", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future?> getDirectoryNotes({ int pageSize = 1000, int pageNumber = 1, }) async { final queryParams = { 'pageSize': pageSize.toString(), 'pageNumber': pageNumber.toString(), }; final response = await _safeApiCall( ApiEndpoints.getDirectoryNotes, method: 'GET', queryParams: queryParams, ); if (response == null) return null; return _parseAndDecryptResponse(response, label: 'Directory Notes') as Map?; } static Future createBucket({ required String name, required String description, }) async { final payload = { "name": name, "description": description, }; final response = await _safeApiCall( ApiEndpoints.createBucket, method: 'POST', body: payload, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Create Bucket", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future assignEmployeesToBucket({ required String bucketId, required List> employees, }) async { final endpoint = "${ApiEndpoints.assignBucket}/$bucketId"; final response = await _safeApiCall( endpoint, method: 'POST', body: employees, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Assign Employees to Bucket", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future updateBucket({ required String id, required String name, required String description, }) async { final payload = { "id": id, "name": name, "description": description, }; final endpoint = "${ApiEndpoints.updateBucket}/$id"; final response = await _safeApiCall( endpoint, method: 'PUT', body: payload, ); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Update Bucket", returnFullResponse: true); return parsed != null && parsed['success'] == true; } static Future deleteBucket(String id) async { final endpoint = "${ApiEndpoints.updateBucket}/$id"; final response = await _safeApiCall(endpoint, method: 'DELETE'); if (response == null) return false; final parsed = _parseAndDecryptResponse(response, label: "Delete Bucket", returnFullResponse: true); return parsed != null && parsed['success'] == true; } }