diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 4d36d63..72e992a 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,9 +1,9 @@ class ApiEndpoints { - static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; - // static const String baseUrl = "https://mapi.marcoaiot.com/api"; -// static const String baseUrl = "https://api.onfieldwork.com/api"; + static const String baseUrl = "https://mapi.marcoaiot.com/api"; + // static const String baseUrl = "https://api.onfieldwork.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; diff --git a/lib/helpers/services/api_service copy.dart b/lib/helpers/services/api_service copy.dart new file mode 100644 index 0000000..dbf41d2 --- /dev/null +++ b/lib/helpers/services/api_service copy.dart @@ -0,0 +1,4051 @@ +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'; + +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/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/helpers/services/app_logger.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'; +import 'package:on_field_work/helpers/utils/encryption_helper.dart'; + +class ApiService { + static const bool enableLogs = true; + static const Duration extendedTimeout = Duration(seconds: 60); + + static Future _getToken() async { + final token = LocalStorage.getJwtToken(); + + if (token == null) { + logSafe("No JWT token found. Logging out..."); + await LocalStorage.logout(); + return null; + } + + try { + if (JwtDecoder.isExpired(token)) { + logSafe("Access token is expired. Attempting refresh..."); + final refreshed = await AuthService.refreshToken(); + if (refreshed) { + return LocalStorage.getJwtToken(); + } else { + logSafe("Token refresh failed. Logging out immediately..."); + await LocalStorage.logout(); + return null; + } + } + + final expirationDate = JwtDecoder.getExpirationDate(token); + final now = DateTime.now(); + final difference = expirationDate.difference(now); + + if (difference.inMinutes < 2) { + logSafe( + "Access token is about to expire in ${difference.inSeconds}s. Refreshing..."); + final refreshed = await AuthService.refreshToken(); + if (refreshed) { + return LocalStorage.getJwtToken(); + } else { + logSafe("Token refresh failed (near expiry). Logging out..."); + await LocalStorage.logout(); + return null; + } + } + } catch (e) { + logSafe("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', + }; + + static void _log(String message, {LogLevel level = LogLevel.info}) { + if (enableLogs) { + logSafe(message, level: level); + } + } + + static dynamic _parseResponse(http.Response response, {String label = ''}) { + _log("$label Encrypted Response: ${response.body}"); // Log encrypted body + + // --- ⚠️ START of Decryption Change ⚠️ --- + final decryptedData = + decryptResponse(response.body); // Decrypt the Base64 string + + if (decryptedData == null) { + _log("Decryption failed for [$label]. Cannot parse response."); + return null; + } + // If decryptedData is a Map/List (JSON), use it directly. + // If it's a plain String, you'll need to decode it to JSON. + + // Assuming the decrypted result is a Map/List (JSON), as per your API structure: + final json = decryptedData; + + // Now proceed with your existing logic using the decrypted JSON object + if (response.statusCode == 200 && json is Map && json['success'] == true) { + _log("$label Decrypted Data: ${json['data']}"); + return json['data']; + } + + // Handle error cases using the decrypted data + if (json is Map) { + _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); + } else { + _log("API Error [$label]: Decrypted response not a map: $json"); + } + + // --- ⚠️ END of Decryption Change ⚠️ --- + return null; + } + + static dynamic _parseResponseForAllData(http.Response response, + {String label = ''}) { + _log("$label Encrypted Response: ${response.body}"); + + // --- ⚠️ START of Decryption Change ⚠️ --- + final body = response.body.trim(); + if (body.isEmpty) { + _log("Empty response body for [$label]"); + return null; + } + + final json = decryptResponse(body); // Decrypt and auto-decode JSON + + if (json == null) { + _log("Decryption failed for [$label]. Cannot parse response."); + return null; + } + + if (json is Map && response.statusCode == 200 && json['success'] == true) { + _log("$label Decrypted JSON: $json"); + return json; // Return the full JSON map + } + + // Handle error cases + if (json is Map) { + _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); + } else { + _log("API Error [$label]: Decrypted response not a map: $json"); + } + + // --- ⚠️ END of Decryption Change ⚠️ --- + return null; + } + + static Future _getRequest( + String endpoint, { + Map? queryParams, + bool hasRetried = false, + }) async { + String? token = await _getToken(); + if (token == null) { + logSafe("Token is null. Forcing logout from GET request.", + level: LogLevel.error); + await LocalStorage.logout(); + return null; + } + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") + .replace(queryParameters: queryParams); + + logSafe("Initiating GET request", level: LogLevel.debug); + logSafe("URL: $uri", level: LogLevel.debug); + logSafe("Query Parameters: ${queryParams ?? {}}", level: LogLevel.debug); + logSafe("Headers: ${_headers(token)}", level: LogLevel.debug); + + try { + final response = await http + .get(uri, headers: _headers(token)) + .timeout(extendedTimeout); + + logSafe("Response Status: ${response.statusCode}", level: LogLevel.debug); + logSafe("Response Body: ${response.body}", level: LogLevel.debug); + + if (response.statusCode == 401 && !hasRetried) { + logSafe("Unauthorized (401). Attempting token refresh...", + level: LogLevel.warning); + + if (await AuthService.refreshToken()) { + logSafe("Token refresh succeeded. Retrying request...", + level: LogLevel.info); + return await _getRequest( + endpoint, + queryParams: queryParams, + hasRetried: true, + ); + } + + logSafe("Token refresh failed. Logging out user.", + level: LogLevel.error); + await LocalStorage.logout(); + } + + return response; + } catch (e) { + logSafe("HTTP GET Exception: $e", level: LogLevel.error); + return null; + } + } + + static Future _postRequest( + String endpoint, + dynamic body, { + Duration customTimeout = extendedTimeout, + bool hasRetried = false, + }) async { + String? token = await _getToken(); + if (token == null) return null; + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); + logSafe( + "POST $uri\nHeaders: ${_headers(token)}\nBody: $body", + ); + + try { + final response = await http + .post(uri, headers: _headers(token), body: jsonEncode(body)) + .timeout(customTimeout); + + if (response.statusCode == 401 && !hasRetried) { + logSafe("Unauthorized POST. Attempting token refresh..."); + if (await AuthService.refreshToken()) { + return await _postRequest(endpoint, body, + customTimeout: customTimeout, hasRetried: true); + } + } + return response; + } catch (e) { + logSafe("HTTP POST Exception: $e", level: LogLevel.error); + return null; + } + } + + static Future _putRequest( + String endpoint, + dynamic body, { + Map? additionalHeaders, + Duration customTimeout = extendedTimeout, + bool hasRetried = false, + }) async { + String? token = await _getToken(); + if (token == null) return null; + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); + logSafe( + "PUT $uri\nHeaders: ${_headers(token)}\nBody: $body", + ); + final headers = { + ..._headers(token), + if (additionalHeaders != null) ...additionalHeaders, + }; + + logSafe( + "PUT $uri\nHeaders: $headers\nBody: $body", + ); + + try { + final response = await http + .put(uri, headers: headers, body: jsonEncode(body)) + .timeout(customTimeout); + + if (response.statusCode == 401 && !hasRetried) { + logSafe("Unauthorized PUT. Attempting token refresh..."); + if (await AuthService.refreshToken()) { + return await _putRequest(endpoint, body, + additionalHeaders: additionalHeaders, + customTimeout: customTimeout, + hasRetried: true); + } + } + + return response; + } catch (e) { + logSafe("HTTP PUT Exception: $e", level: LogLevel.error); + return null; + } + } + + static Future _deleteRequest( + String endpoint, { + Map? additionalHeaders, + Duration customTimeout = extendedTimeout, + bool hasRetried = false, + }) async { + String? token = await _getToken(); + if (token == null) return null; + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); + final headers = { + ..._headers(token), + if (additionalHeaders != null) ...additionalHeaders, + }; + + logSafe("DELETE $uri\nHeaders: $headers"); + + try { + final response = + await http.delete(uri, headers: headers).timeout(customTimeout); + + if (response.statusCode == 401 && !hasRetried) { + logSafe("Unauthorized DELETE. Attempting token refresh..."); + if (await AuthService.refreshToken()) { + return await _deleteRequest( + endpoint, + additionalHeaders: additionalHeaders, + customTimeout: customTimeout, + hasRetried: true, + ); + } + } + + return response; + } catch (e) { + logSafe("HTTP DELETE Exception: $e", level: LogLevel.error); + return null; + } + } + + /// ============================================ + /// GET PURCHASE INVOICE OVERVIEW (Dashboard) + /// ============================================ + static Future getPurchaseInvoiceOverview({ + String? projectId, + }) async { + try { + final queryParams = {}; + if (projectId != null && projectId.isNotEmpty) { + queryParams['projectId'] = projectId; + } + + final response = await _getRequest( + ApiEndpoints.getPurchaseInvoiceOverview, + queryParams: queryParams, + ); + + if (response == null) { + _log("getPurchaseInvoiceOverview: No response from server", + level: LogLevel.error); + return null; + } + + final parsedJson = _parseResponseForAllData( + response, + label: "PurchaseInvoiceOverview", + ); + + if (parsedJson == null) return null; + + return PurchaseInvoiceOverviewResponse.fromJson(parsedJson); + } catch (e, stack) { + _log("Exception in getPurchaseInvoiceOverview: $e\n$stack", + level: LogLevel.error); + return null; + } + } + + /// ============================================ + /// GET COLLECTION OVERVIEW (Dashboard) + /// ============================================ + static Future getCollectionOverview({ + String? projectId, + }) async { + try { + // Build query params (only add projectId if not null) + final queryParams = {}; + if (projectId != null && projectId.isNotEmpty) { + queryParams['projectId'] = projectId; + } + + final response = await _getRequest( + ApiEndpoints.getCollectionOverview, + queryParams: queryParams, + ); + + if (response == null) { + _log("getCollectionOverview: No response from server", + level: LogLevel.error); + return null; + } + + // Parse full JSON (success, message, data, etc.) + final parsedJson = + _parseResponseForAllData(response, label: "CollectionOverview"); + + if (parsedJson == null) return null; + + return CollectionOverviewResponse.fromJson(parsedJson); + } catch (e, stack) { + _log("Exception in getCollectionOverview: $e\n$stack", + level: LogLevel.error); + return null; + } + } + +// Infra Project Module APIs + + /// ================================ + /// GET INFRA PROJECT DETAILS + /// ================================ + static Future getInfraProjectDetails({ + required String projectId, + }) async { + final endpoint = "${ApiEndpoints.getInfraProjectDetail}/$projectId"; + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + _log("getInfraProjectDetails: No response from server", + level: LogLevel.error); + return null; + } + + final parsedJson = + _parseResponseForAllData(response, label: "InfraProjectDetails"); + + if (parsedJson == null) return null; + + return ProjectDetailsResponse.fromJson(parsedJson); + } catch (e, stack) { + _log("Exception in getInfraProjectDetails: $e\n$stack", + level: LogLevel.error); + return null; + } + } + + /// ================================ + /// GET INFRA PROJECTS LIST + /// ================================ + static Future getInfraProjectsList({ + int pageSize = 20, + int pageNumber = 1, + String searchString = "", + }) async { + final queryParams = { + "pageSize": pageSize.toString(), + "pageNumber": pageNumber.toString(), + "searchString": searchString, + }; + + try { + final response = await _getRequest( + ApiEndpoints.getInfraProjectsList, + queryParams: queryParams, + ); + + if (response == null) { + _log("getInfraProjectsList: No response from server", + level: LogLevel.error); + return null; + } + + final parsedJson = + _parseResponseForAllData(response, label: "InfraProjectsList"); + + if (parsedJson == null) return null; + + return ProjectsResponse.fromJson(parsedJson); + } catch (e, stack) { + _log("Exception in getInfraProjectsList: $e\n$stack", + level: LogLevel.error); + return null; + } + } + + static Future getJobCommentList({ + required String jobTicketId, + int pageNumber = 1, + int pageSize = 20, + }) async { + final queryParams = { + 'jobTicketId': jobTicketId, + 'pageNumber': pageNumber.toString(), + 'pageSize': pageSize.toString(), + }; + + try { + final response = await _getRequest( + ApiEndpoints.getJobCommentList, + queryParams: queryParams, + ); + + if (response == null) { + _log("getJobCommentList: No response from server", + level: LogLevel.error); + return null; + } + + final parsedJson = + _parseResponseForAllData(response, label: "JobCommentList"); + if (parsedJson == null) return null; + + return JobCommentResponse.fromJson(parsedJson); + } catch (e, stack) { + _log("Exception in getJobCommentList: $e\n$stack", level: LogLevel.error); + return null; + } + } + + static Future addJobComment({ + required String jobTicketId, + required String comment, + List> attachments = const [], + }) async { + final body = { + "jobTicketId": jobTicketId, + "comment": comment, + "attachments": attachments, + }; + + try { + final response = await _postRequest( + ApiEndpoints.addJobComment, + body, + ); + + if (response == null) { + _log("addJobComment: No response from server", level: LogLevel.error); + return false; + } + + // Handle 201 Created as success manually + if (response.statusCode == 201) { + _log("AddJobComment: Comment added successfully (201).", + level: LogLevel.info); + return true; + } + + // Otherwise fallback to existing _parseResponse + final parsed = _parseResponse(response, label: "AddJobComment"); + + if (parsed != null && parsed['success'] == true) { + _log("AddJobComment: Comment added successfully.", + level: LogLevel.info); + return true; + } else { + _log( + "AddJobComment failed: ${parsed?['message'] ?? 'Unknown error'}", + level: LogLevel.error, + ); + return false; + } + } catch (e, stack) { + _log("Exception in addJobComment: $e\n$stack", level: LogLevel.error); + return false; + } + } + + static Future?> getMasterJobStatus({ + required String statusId, + required String projectId, + }) async { + final queryParams = { + 'statusId': statusId, + 'projectId': projectId, + }; + + try { + final response = await _getRequest( + ApiEndpoints.getMasterJobStatus, + queryParams: queryParams, + ); + + if (response == null) { + _log("getMasterJobStatus: No response received."); + return null; + } + + final parsedJson = + _parseResponseForAllData(response, label: "MasterJobStatus"); + + if (parsedJson == null) return null; + + // Directly parse JobStatus list + final dataList = (parsedJson['data'] as List?) + ?.map((e) => JobStatus.fromJson(e)) + .toList(); + + return dataList; + } catch (e, stack) { + _log("Exception in getMasterJobStatus: $e\n$stack", + level: LogLevel.error); + return null; + } + } + + /// Fetch Service Project Branches with full response + 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"; + + try { + final response = await _getRequest( + endpoint, + queryParams: queryParams, + ); + + if (response == null) { + _log("getServiceProjectBranchesFull: No response received."); + return null; + } + + final parsedJson = _parseResponseForAllData( + response, + label: "ServiceProjectBranchesFull", + ); + + if (parsedJson == null) return null; + + return ServiceProjectBranchesResponse.fromJson(parsedJson); + } catch (e, stack) { + _log( + "Exception in getServiceProjectBranchesFull: $e\n$stack", + level: LogLevel.error, + ); + return null; + } + } + + // Service Project Module APIs + static Future?> getTeamRoles() async { + try { + final response = await _getRequest(ApiEndpoints.getTeamRoles); + + if (response == null) { + _log("getTeamRoles: No response received."); + return null; + } + + final parsedJson = _parseResponseForAllData(response, label: "TeamRoles"); + if (parsedJson == null) return null; + + // Map the 'data' array to List + final List dataList = parsedJson['data'] as List; + return dataList + .map((e) => TeamRole.fromJson(e as Map)) + .toList(); + } catch (e, stack) { + _log("Exception in getTeamRoles: $e\n$stack", level: LogLevel.error); + return null; + } + } + + /// Fetch Service Project Allocation List + + static Future?> + getServiceProjectAllocationList({ + required String projectId, + bool isActive = true, + }) async { + final queryParams = { + 'projectId': projectId, + 'isActive': isActive.toString(), + }; + + try { + final response = await _getRequest( + ApiEndpoints.getServiceProjectUpateJobAllocationList, + queryParams: queryParams, + ); + + if (response == null) { + _log("getServiceProjectAllocationList: No response received."); + return null; + } + + final parsedJson = _parseResponseForAllData(response, + label: "ServiceProjectAllocationList"); + if (parsedJson == null) return null; + + final dataList = (parsedJson['data'] as List) + .map((e) => ServiceProjectAllocation.fromJson(e)) + .toList(); + + return dataList; + } catch (e, stack) { + _log("Exception in getServiceProjectAllocationList: $e\n$stack"); + return null; + } + } + + /// Manage Service Project Allocation + static Future manageServiceProjectAllocation({ + required List> payload, + }) async { + try { + final response = await _postRequest( + ApiEndpoints.manageServiceProjectUpateJobAllocation, + payload, + ); + + if (response == null) { + _log("manageServiceProjectAllocation: No response received.", + level: LogLevel.error); + return false; + } + + final json = jsonDecode(response.body); + if (json['success'] == true) { + _log( + "Service Project Allocation updated successfully: ${json['data']}"); + return true; + } else { + _log( + "Failed to update Service Project Allocation: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + return false; + } + } catch (e, stack) { + _log("Exception during manageServiceProjectAllocation: $e", + level: LogLevel.error); + _log("StackTrace: $stack", level: LogLevel.debug); + return false; + } + } + + static Future getJobAttendanceLog({ + required String attendanceId, + }) async { + final endpoint = + "${ApiEndpoints.serviceProjectUpateJobAttendanceLog}/$attendanceId"; + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + _log("getJobAttendanceLog: No response received."); + return null; + } + + final parsedJson = + _parseResponseForAllData(response, label: "JobAttendanceLog"); + if (parsedJson == null) return null; + + return JobAttendanceResponse.fromJson(parsedJson); + } catch (e, stack) { + _log("Exception in getJobAttendanceLog: $e\n$stack"); + return null; + } + } + + /// Update Service Project Job Attendance + static Future updateServiceProjectJobAttendance({ + required Map payload, + }) async { + const endpoint = ApiEndpoints.serviceProjectUpateJobAttendance; + + logSafe("Updating Service Project Job Attendance with payload: $payload"); + + try { + final response = await _postRequest(endpoint, payload); + + if (response == null) { + logSafe("Update Job Attendance failed: null response", + level: LogLevel.error); + return false; + } + + logSafe("Update Job Attendance response status: ${response.statusCode}"); + logSafe("Update Job Attendance response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Job Attendance updated successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to update Job Attendance: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + return false; + } + } catch (e, stack) { + logSafe("Exception during updateServiceProjectJobAttendance: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; + } + } + + /// Edit a Service Project Job + static Future editServiceProjectJobApi({ + required String jobId, + required List> operations, + }) async { + final endpoint = "${ApiEndpoints.editServiceProjectJob}/$jobId"; + + logSafe("Editing Service Project Job: $jobId with operations: $operations"); + + try { + // PATCH request is usually similar to PUT, but with http.patch + String? token = await _getToken(); + if (token == null) return false; + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); + + final headers = _headers(token); + + final response = await http + .patch(uri, headers: headers, body: jsonEncode(operations)) + .timeout(extendedTimeout); + + logSafe( + "Edit Service Project Job response status: ${response.statusCode}"); + logSafe("Edit Service Project Job response body: ${response.body}"); + + final json = jsonDecode(response.body); + + if (response.statusCode == 200 && json['success'] == true) { + logSafe("Service Project Job edited successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to edit Service Project Job: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + return false; + } + } catch (e, stack) { + logSafe("Exception during editServiceProjectJobApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; + } + } + + /// Get details for a single Service Project Job + static Future getServiceProjectJobDetailApi( + String jobId) async { + final endpoint = "${ApiEndpoints.getServiceProjectJobDetail}/$jobId"; + logSafe("Fetching Job Detail for Job ID: $jobId"); + + try { + final response = await _getRequest(endpoint); + if (response == null) { + logSafe("Service Project Job Detail request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Job Detail", + ); + + if (jsonResponse != null) { + return JobDetailsResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectJobDetailApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + 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 { + const endpoint = ApiEndpoints.createServiceProjectJob; + logSafe("Creating Service Project Job for projectId: $projectId"); + + final body = { + "title": title, + "description": description, + "projectId": projectId, + "assignees": assignees, + "startDate": startDate.toIso8601String(), + "dueDate": dueDate.toIso8601String(), + "tags": tags, + "projectBranchId": branchId, + }; + + try { + final response = await _postRequest(endpoint, body); + + if (response == null) return null; + + final json = jsonDecode(response.body); + + if (json['success'] == true) { + final jobId = json['data']?['id']; + logSafe("Service Project Job created successfully: $jobId"); + return jobId; + } + + return null; + } catch (e, stack) { + logSafe("Exception during createServiceProjectJobApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return null; + } + } + + /// Get Service Project Job List (Active or Archived) + static Future getServiceProjectJobListApi({ + required String projectId, + int pageNumber = 1, + int pageSize = 20, + bool isActive = true, + bool isArchive = false, // new parameter to fetch archived jobs + }) async { + const endpoint = ApiEndpoints.getServiceProjectJobList; + logSafe( + "Fetching Job List for Service Project: $projectId | isActive: $isActive | isArchive: $isArchive", + ); + + try { + final queryParams = { + 'projectId': projectId, + 'pageNumber': pageNumber.toString(), + 'pageSize': pageSize.toString(), + 'isActive': isActive.toString(), + if (isArchive) 'isArchive': 'true', + }; + + final response = await _getRequest(endpoint, queryParams: queryParams); + + if (response == null) { + logSafe( + "Service Project Job List request failed: null response", + level: LogLevel.error, + ); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: isArchive + ? "Archived Service Project Job List" + : "Active Service Project Job List", + ); + + if (jsonResponse != null) { + return JobResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe( + "Exception during getServiceProjectJobListApi: $e", + level: LogLevel.error, + ); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + +// API to get all employees from basic + static Future?> allEmployeesBasic({ + bool allEmployee = true, + }) async { + final queryParams = {}; + + // Always include allEmployee parameter + queryParams['allEmployee'] = allEmployee.toString(); + + final response = await _getRequest( + ApiEndpoints.getEmployeesWithoutPermission, + queryParams: queryParams, + ); + + if (response != null) { + return _parseResponse(response, label: ' All Employees Basic'); + } + + return null; + } + + /// Get details of a single service project + static Future getServiceProjectDetailApi( + String projectId) async { + final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; + logSafe("Fetching details for Service Project ID: $projectId"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe( + "Service Project Detail request failed: null response", + level: LogLevel.error, + ); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Detail", + ); + + if (jsonResponse != null) { + return ServiceProjectDetailModel.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe( + "Exception during getServiceProjectDetailApi: $e", + level: LogLevel.error, + ); + logSafe( + "StackTrace: $stack", + level: LogLevel.debug, + ); + } + + return null; + } + + /// Get Service Project List + static Future getServiceProjectsListApi({ + int pageNumber = 1, + int pageSize = 20, + }) async { + const endpoint = ApiEndpoints.getServiceProjectsList; + logSafe("Fetching Service Project List"); + + try { + final queryParams = { + 'pageNumber': pageNumber.toString(), + 'pageSize': pageSize.toString(), + }; + + final response = await _getRequest(endpoint, queryParams: queryParams); + + if (response == null) { + logSafe("Service Project List request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project List", + ); + + if (jsonResponse != null) { + return ServiceProjectListModel.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectsListApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Edit Expense Payment Request + 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, + }; + + try { + final response = await _putRequest(endpoint, body); + + if (response == null) { + logSafe("Edit Expense Payment Request failed: null response", + level: LogLevel.error); + return false; + } + + logSafe( + "Edit Expense Payment Request response status: ${response.statusCode}"); + logSafe("Edit Expense Payment Request response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe( + "Expense Payment Request edited successfully: ${json['data'] ?? 'No data'}"); + return true; + } else { + logSafe( + "Failed to edit Expense Payment Request: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + return false; + } + } catch (e, stack) { + logSafe("Exception during editExpensePaymentRequestApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; + } + } + + /// Create Expense for Payment Request + 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 { + const endpoint = ApiEndpoints.createExpenseforPR; + + final body = { + "paymentModeId": paymentModeId, + "location": location, + "gstNumber": gstNumber, + "statusId": statusId, + "comment": comment, + "paymentRequestId": paymentRequestId, + "billAttachments": billAttachments, + }; + + try { + final response = await _postRequest(endpoint, body); + + if (response == null) { + logSafe("Create Expense for PR failed: null response", + level: LogLevel.error); + return false; + } + + logSafe("Create Expense for PR response status: ${response.statusCode}"); + logSafe("Create Expense for PR response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe( + "Expense for Payment Request created successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to create Expense for Payment Request: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + return false; + } + } catch (e, stack) { + logSafe("Exception during createExpenseForPRApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; + } + } + + /// Update Expense Payment Request Status + 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 { + const endpoint = ApiEndpoints.updateExpensePaymentRequestStatus; + logSafe("Updating Payment Request Status for ID: $paymentRequestId"); + + final body = { + "paymentRequestId": paymentRequestId, + "statusId": statusId, + "comment": comment, + "paidTransactionId": paidTransactionId, + "paidById": paidById, + "paidAt": paidAt?.toIso8601String(), + "baseAmount": baseAmount, + "taxAmount": taxAmount, + "tdsPercentage": tdsPercentage ?? "0", + }; + + try { + final response = await _postRequest(endpoint, body); + + if (response == null) { + logSafe("Update Payment Request Status failed: null response", + level: LogLevel.error); + return false; + } + + logSafe( + "Update Payment Request Status response: ${response.statusCode} -> ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Payment Request status updated successfully!"); + return true; + } else { + logSafe( + "Failed to update Payment Request Status: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + return false; + } + } catch (e, stack) { + logSafe("Exception during updateExpensePaymentRequestStatusApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; + } + } + + /// Get Expense Payment Request Detail by ID + static Future getExpensePaymentRequestDetailApi( + String paymentRequestId) async { + final endpoint = + "${ApiEndpoints.getExpensePaymentRequestDetails}/$paymentRequestId"; + logSafe( + "Fetching Expense Payment Request Detail for ID: $paymentRequestId"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Expense Payment Request Detail request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Expense Payment Request Detail", + ); + + if (jsonResponse != null) { + return PaymentRequestDetail.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getExpensePaymentRequestDetailApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + static Future + getExpensePaymentRequestFilterApi() async { + const endpoint = ApiEndpoints.getExpensePaymentRequestFilter; + logSafe("Fetching Expense Payment Request Filter"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Expense Payment Request Filter request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Expense Payment Request Filter", + ); + + if (jsonResponse != null) { + return PaymentRequestFilter.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getExpensePaymentRequestFilterApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Get Expense Payment Request List + static Future getExpensePaymentRequestListApi({ + bool isActive = true, + int pageSize = 20, + int pageNumber = 1, + Map? filter, + String searchString = '', + }) async { + const endpoint = ApiEndpoints.getExpensePaymentRequestList; + logSafe("Fetching Expense Payment Request List"); + + try { + 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 _getRequest(endpoint, queryParams: queryParams); + + if (response == null) { + logSafe("Expense Payment Request List request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Expense Payment Request List", + ); + + if (jsonResponse != null) { + return PaymentRequestResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getExpensePaymentRequestListApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Create Expense Payment Request (Project API style) + 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 { + const endpoint = ApiEndpoints.createExpensePaymentRequest; + + final body = { + "title": title, + "projectId": projectId, + "expenseCategoryId": expenseCategoryId, + "currencyId": currencyId, + "payee": payee, + "amount": amount, + "dueDate": dueDate?.toIso8601String(), + "description": description, + "isAdvancePayment": isAdvancePayment, + "billAttachments": billAttachments, + }; + + try { + final response = await _postRequest(endpoint, body); + if (response == null) { + logSafe("Create Payment Request failed: null response", + level: LogLevel.error); + return false; + } + + logSafe("Create Payment Request response status: ${response.statusCode}"); + logSafe("Create Payment Request response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Payment Request created successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to create Payment Request: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + return false; + } + } catch (e, stack) { + logSafe("Exception during createExpensePaymentRequestApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; + } + } + + /// Fetch hierarchy list for an employee + static Future?> getOrganizationHierarchyList( + String employeeId) async { + if (employeeId.isEmpty) throw ArgumentError('employeeId must not be empty'); + final endpoint = "${ApiEndpoints.getOrganizationHierarchyList}/$employeeId"; + + return _getRequest(endpoint).then( + (res) => res != null + ? _parseResponse(res, label: 'Organization Hierarchy List') + : null, + ); + } + + /// Manage (create/update) organization hierarchy (assign reporters) for an employee + /// payload is a List> with objects like: + /// { "reportToId": "", "isPrimary": true, "isActive": true } + 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"; + + logSafe("manageOrganizationHierarchy for $employeeId payload: $payload"); + + try { + final response = await _postRequest(endpoint, payload); + if (response == null) { + logSafe("Manage hierarchy failed: null response", + level: LogLevel.error); + return false; + } + + logSafe("Manage hierarchy response status: ${response.statusCode}"); + logSafe("Manage hierarchy response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Manage hierarchy succeeded"); + return true; + } + + logSafe("Manage hierarchy failed: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.error); + return false; + } catch (e, stack) { + logSafe("Exception while manageOrganizationHierarchy: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; + } + } + + /// Get Master Currencies + static Future getMasterCurrenciesApi() async { + const endpoint = ApiEndpoints.getMasterCurrencies; + logSafe("Fetching Master Currencies"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Master Currencies request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Master Currencies"); + if (jsonResponse != null) { + return CurrencyListResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getMasterCurrenciesApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Get Master Expense Categories + static Future + getMasterExpenseCategoriesApi() async { + const endpoint = ApiEndpoints.getMasterExpensesCategories; + logSafe("Fetching Master Expense Categories"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Master Expense Categories request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData(response, + label: "Master Expense Categories"); + if (jsonResponse != null) { + return ExpenseCategoryResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getMasterExpenseCategoriesApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Get Expense Payment Request Payee + static Future + getExpensePaymentRequestPayeeApi() async { + const endpoint = ApiEndpoints.getExpensePaymentRequestPayee; + logSafe("Fetching Expense Payment Request Payees"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Expense Payment Request Payee request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData(response, + label: "Expense Payment Request Payee"); + if (jsonResponse != null) { + return PaymentRequestPayeeResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getExpensePaymentRequestPayeeApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Get Monthly Expense Report (categoryId is optional) + static Future + getDashboardMonthlyExpensesApi({ + String? categoryId, + int months = 12, + }) async { + const endpoint = ApiEndpoints.getDashboardMonthlyExpenses; + logSafe("Fetching Dashboard Monthly Expenses for last $months months"); + + try { + final queryParams = { + 'months': months.toString(), + if (categoryId != null && categoryId.isNotEmpty) + 'categoryId': categoryId, + }; + + final response = await _getRequest( + endpoint, + queryParams: queryParams, + ); + + if (response == null) { + logSafe("Monthly Expense request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData(response, + label: "Dashboard Monthly Expenses"); + + if (jsonResponse != null) { + return DashboardMonthlyExpenseResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getDashboardMonthlyExpensesApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Get Expense Category Report + static Future getExpenseTypeReportApi({ + required String projectId, + required DateTime startDate, + required DateTime endDate, + }) async { + const endpoint = ApiEndpoints.getExpenseTypeReport; + logSafe("Fetching Expense Category Report for projectId: $projectId"); + + try { + final response = await _getRequest( + endpoint, + queryParams: { + 'projectId': projectId, + 'startDate': startDate.toIso8601String(), + 'endDate': endDate.toIso8601String(), + }, + ); + + if (response == null) { + logSafe("Expense Category Report request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Expense Category Report"); + + if (jsonResponse != null) { + return ExpenseTypeReportResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getExpenseTypeReportApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Get Pending Expenses + static Future getPendingExpensesApi({ + required String projectId, + }) async { + const endpoint = ApiEndpoints.getPendingExpenses; + logSafe("Fetching Pending Expenses for projectId: $projectId"); + + try { + final response = await _getRequest( + endpoint, + queryParams: {'projectId': projectId}, + ); + + if (response == null) { + logSafe("Pending Expenses request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Pending Expenses"); + + if (jsonResponse != null) { + return PendingExpensesResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getPendingExpensesApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Create Project API + 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 { + const endpoint = ApiEndpoints.createProject; + logSafe("Creating project: $name"); + + final Map payload = { + "name": name, + "projectAddress": projectAddress, + "shortName": shortName, + "contactPerson": contactPerson, + "startDate": startDate.toIso8601String(), + "endDate": endDate.toIso8601String(), + "projectStatusId": projectStatusId, + }; + + try { + final response = + await _postRequest(endpoint, payload, customTimeout: extendedTimeout); + + if (response == null) { + logSafe("Create project failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Create project response status: ${response.statusCode}"); + logSafe("Create project response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Project created successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to create project: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + } + } catch (e, stack) { + logSafe("Exception during createProjectApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + /// Get Organizations assigned to a Project + static Future getAssignedOrganizations( + String projectId) async { + final endpoint = "${ApiEndpoints.getAssignedOrganizations}/$projectId"; + logSafe("Fetching organizations assigned to projectId: $projectId"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Assigned Organizations request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Assigned Organizations"); + + if (jsonResponse != null) { + return OrganizationListResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getAssignedOrganizations: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + static Future getAllOrganizations() async { + final endpoint = "${ApiEndpoints.getAllOrganizations}"; + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("All Organizations request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "All Organizations"); + + if (jsonResponse != null) { + return AllOrganizationListResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getAllOrganizations: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + //// Get Services assigned to a Project + static Future getAssignedServices( + String projectId) async { + final endpoint = "${ApiEndpoints.getAssignedServices}/$projectId"; + logSafe("Fetching services assigned to projectId: $projectId"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Assigned Services request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Assigned Services"); + + if (jsonResponse != null) { + return ServiceListResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getAssignedServices: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + static Future postLogsApi(List> logs) async { + const endpoint = "${ApiEndpoints.uploadLogs}"; + logSafe("Posting logs... count=${logs.length}"); + + try { + // Get token directly without triggering logout or refresh + final token = await LocalStorage.getJwtToken(); + if (token == null) { + logSafe("No token available. Skipping logs post.", + level: LogLevel.warning); + return false; + } + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + // Send logs as JSON + final response = await http + .post(uri, headers: headers, body: jsonEncode(logs)) + .timeout(ApiService.extendedTimeout); + + logSafe("Post logs response status: ${response.statusCode}"); + logSafe("Post logs response body: ${response.body}"); + + // --- Decrypt response before parsing --- + final decryptedData = + decryptResponse(response.body); // returns Map/List or null + + if (decryptedData == null) { + logSafe("Decryption failed. Cannot parse logs response.", + level: LogLevel.error); + return false; + } + + // Expecting decrypted data to be a Map with 'success' field + if (response.statusCode == 200 && + decryptedData is Map && + decryptedData['success'] == true) { + logSafe("Logs posted successfully."); + return true; + } else { + final errorMsg = decryptedData is Map + ? decryptedData['message'] ?? 'Unknown error' + : 'Decrypted response not a Map: $decryptedData'; + logSafe("Failed to post logs: $errorMsg", level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during postLogsApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + /// Verify Document API + static Future verifyDocumentApi({ + required String id, + bool isVerify = true, + }) async { + final endpoint = "${ApiEndpoints.verifyDocument}/$id"; + final queryParams = {"isVerify": isVerify.toString()}; + logSafe("Verifying document with id: $id | isVerify: $isVerify"); + + try { + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") + .replace(queryParameters: queryParams); + + String? token = await _getToken(); + if (token == null) return false; + + final headers = _headers(token); + logSafe("POST (verify) $uri\nHeaders: $headers"); + + final response = + await http.post(uri, headers: headers).timeout(extendedTimeout); + + if (response.statusCode == 401) { + logSafe("Unauthorized VERIFY. Attempting token refresh..."); + if (await AuthService.refreshToken()) { + return await verifyDocumentApi(id: id, isVerify: isVerify); + } + } + + logSafe("Verify document response status: ${response.statusCode}"); + logSafe("Verify document response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Document verify success: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to verify document: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + } + } catch (e, stack) { + logSafe("Exception during verifyDocumentApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + /// Get Pre-Signed URL for Old Version + static Future getPresignedUrlApi(String versionId) async { + final endpoint = "${ApiEndpoints.getDocumentVersion}/$versionId"; + logSafe("Fetching Pre-Signed URL for versionId: $versionId"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Pre-Signed URL request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Pre-Signed URL"); + + if (jsonResponse != null) { + return jsonResponse['data'] as String?; + } + } catch (e, stack) { + logSafe("Exception during getPresignedUrlApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Delete (Soft Delete / Deactivate) Document API + static Future deleteDocumentApi({ + required String id, + bool isActive = false, // default false = delete + }) async { + final endpoint = "${ApiEndpoints.deleteDocument}/$id"; + final queryParams = {"isActive": isActive.toString()}; + logSafe("Deleting document with id: $id | isActive: $isActive"); + + try { + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") + .replace(queryParameters: queryParams); + + String? token = await _getToken(); + if (token == null) return false; + + final headers = _headers(token); + logSafe("DELETE (PUT/POST style) $uri\nHeaders: $headers"); + + // some backends use PUT instead of DELETE for soft deletes + final response = + await http.delete(uri, headers: headers).timeout(extendedTimeout); + + if (response.statusCode == 401) { + logSafe("Unauthorized DELETE. Attempting token refresh..."); + if (await AuthService.refreshToken()) { + return await deleteDocumentApi(id: id, isActive: isActive); + } + } + + logSafe("Delete document response status: ${response.statusCode}"); + logSafe("Delete document response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Document delete/update success: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to delete document: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + } + } catch (e, stack) { + logSafe("Exception during deleteDocumentApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + /// Edit Document API + static Future editDocumentApi({ + required String id, + required String name, + required String documentId, + String? description, + List> tags = const [], + Map? attachment, // 👈 can be null + }) async { + final endpoint = "${ApiEndpoints.editDocument}/$id"; + logSafe("Editing document with id: $id"); + + final Map payload = { + "id": id, + "name": name, + "documentId": documentId, + "description": description ?? "", + "tags": tags.isNotEmpty + ? tags + : [ + {"name": "default", "isActive": true} + ], + "attachment": attachment, // 👈 null or object + }; + + try { + final response = + await _putRequest(endpoint, payload, customTimeout: extendedTimeout); + + if (response == null) { + logSafe("Edit document failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Edit document response status: ${response.statusCode}"); + logSafe("Edit document response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Document edited successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to edit document: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + } + } catch (e, stack) { + logSafe("Exception during editDocumentApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + /// Get List of Versions by ParentAttachmentId + 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(), + }; + + logSafe( + "Fetching document versions for parentAttachmentId: $parentAttachmentId"); + + try { + final response = await _getRequest(endpoint, queryParams: queryParams); + + if (response == null) { + logSafe("Document versions request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Document Versions"); + + if (jsonResponse != null) { + return DocumentVersionsResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getDocumentVersionsApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Get Document Details by ID + static Future getDocumentDetailsApi( + String documentId) async { + final endpoint = "${ApiEndpoints.getDocumentDetails}/$documentId"; + logSafe("Fetching document details for id: $documentId"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Document details request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Document Details"); + + if (jsonResponse != null) { + return DocumentDetailsResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getDocumentDetailsApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Get Document Types by CategoryId + static Future getDocumentTypesByCategoryApi( + String documentCategoryId) async { + const endpoint = ApiEndpoints.getDocumentTypesByCategory; + + logSafe("Fetching document types for category: $documentCategoryId"); + + try { + final response = await _getRequest( + endpoint, + queryParams: {"documentCategoryId": documentCategoryId}, + ); + + if (response == null) { + logSafe("Document types by category request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData(response, + label: "Document Types by Category"); + + if (jsonResponse != null) { + return DocumentTypesResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getDocumentTypesByCategoryApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Get Master Document Types (Category Types) + static Future getMasterDocumentTypesApi() async { + const endpoint = ApiEndpoints.getMasterDocumentCategories; + logSafe("Fetching master document types..."); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Document types request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Master Document Types"); + + if (jsonResponse != null) { + return DocumentTypesResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getMasterDocumentTypesApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Upload Document API + 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 { + const endpoint = ApiEndpoints.uploadDocument; + logSafe("Uploading document: $name for entity: $entityId"); + + final Map 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} + ], + }; + + try { + final response = + await _postRequest(endpoint, payload, customTimeout: extendedTimeout); + + if (response == null) { + logSafe("Upload document failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Upload document response status: ${response.statusCode}"); + logSafe("Upload document response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Document uploaded successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to upload document: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + } + } catch (e, stack) { + logSafe("Exception during uploadDocumentApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + /// Get Master Document Tags + static Future getMasterDocumentTagsApi() async { + const endpoint = ApiEndpoints.getMasterDocumentTags; + logSafe("Fetching master document tags..."); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Tags request failed: null response", level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Master Document Tags"); + + if (jsonResponse != null) { + return TagResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getMasterDocumentTagsApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Get Document List by EntityTypeId and EntityId + 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(), + }; + + logSafe( + "Fetching document list for entityTypeId: $entityTypeId, entityId: $entityId"); + + try { + final response = await _getRequest(endpoint, queryParams: queryParams); + + if (response == null) { + logSafe("Document list request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Document List"); + + if (jsonResponse != null) { + return DocumentsResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getDocumentListApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Get Document Filters by EntityTypeId + static Future getDocumentFilters( + String entityTypeId) async { + final endpoint = "${ApiEndpoints.getDocumentFilter}/$entityTypeId"; + logSafe("Fetching document filters for entityTypeId: $entityTypeId"); + + try { + final response = await _getRequest(endpoint, queryParams: null); + + if (response == null) { + logSafe("Document filter request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Document Filters"); + + if (jsonResponse != null) { + return DocumentFiltersResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getDocumentFilters: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + +// === Menu APIs === // + + /// Get Sidebar Menu API + static Future?> getMenuApi() async { + logSafe("Fetching sidebar menu..."); + + try { + final response = await _getRequest(ApiEndpoints.getDynamicMenu); + if (response == null) { + logSafe("Menu request failed: null response", level: LogLevel.error); + return null; + } + + final body = response.body.trim(); + if (body.isEmpty) { + logSafe("Menu response body is empty", level: LogLevel.error); + return null; + } + + final jsonResponse = jsonDecode(body); + if (jsonResponse is Map) { + if (jsonResponse['success'] == true) { + logSafe("Sidebar menu fetched successfully"); + return jsonResponse; // ✅ return full response + } else { + logSafe( + "Failed to fetch sidebar menu: ${jsonResponse['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + } + } else { + logSafe("Unexpected response structure: $jsonResponse", + level: LogLevel.error); + } + } catch (e, stack) { + logSafe("Exception during getMenuApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + +// === Expense APIs === // + + /// Edit Expense API + static Future editExpenseApi({ + required String expenseId, + required Map payload, + }) async { + final endpoint = "${ApiEndpoints.editExpense}/$expenseId"; + logSafe("Editing expense $expenseId with payload: $payload"); + + try { + final response = await _putRequest( + endpoint, + payload, + customTimeout: extendedTimeout, + ); + + if (response == null) { + logSafe("Edit expense failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Edit expense response status: ${response.statusCode}"); + logSafe("Edit expense response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Expense updated successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to update expense: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + } + } catch (e, stack) { + logSafe("Exception during editExpenseApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + static Future deleteExpense(String expenseId) async { + final endpoint = "${ApiEndpoints.deleteExpense}/$expenseId"; + + try { + final token = await _getToken(); + if (token == null) { + logSafe("Token is null. Cannot proceed with DELETE request.", + level: LogLevel.error); + return false; + } + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); + + logSafe("Sending DELETE request to $uri", level: LogLevel.debug); + + final response = await http + .delete(uri, headers: _headers(token)) + .timeout(extendedTimeout); + + logSafe("DELETE expense response status: ${response.statusCode}"); + logSafe("DELETE expense response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) { + logSafe("Expense deleted successfully."); + return true; + } else { + logSafe( + "Failed to delete expense: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during deleteExpenseApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + /// Get Expense Details API + static Future?> getExpenseDetailsApi({ + required String expenseId, + }) async { + final endpoint = "${ApiEndpoints.getExpenseDetails}/$expenseId"; + logSafe("Fetching expense details for ID: $expenseId"); + + try { + final response = await _getRequest(endpoint); + if (response == null) { + logSafe("Expense details request failed: null response", + level: LogLevel.error); + return null; + } + + final body = response.body.trim(); + if (body.isEmpty) { + logSafe("Expense details response body is empty", + level: LogLevel.error); + return null; + } + + final jsonResponse = jsonDecode(body); + if (jsonResponse is Map) { + if (jsonResponse['success'] == true) { + logSafe("Expense details fetched successfully"); + return jsonResponse['data']; // Return the expense details object + } else { + logSafe( + "Failed to fetch expense details: ${jsonResponse['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + } + } else { + logSafe("Unexpected response structure: $jsonResponse", + level: LogLevel.error); + } + } catch (e, stack) { + logSafe("Exception during getExpenseDetailsApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Update Expense Status API + 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 Map payload = { + "expenseId": expenseId, + "statusId": statusId, + }; + + if (comment != null) payload["comment"] = comment; + if (reimburseTransactionId != null) + payload["reimburseTransactionId"] = reimburseTransactionId; + if (reimburseDate != null) payload["reimburseDate"] = reimburseDate; + if (reimbursedById != null) payload["reimburseById"] = reimbursedById; + if (baseAmount != null) payload["baseAmount"] = baseAmount; + if (taxAmount != null) payload["taxAmount"] = taxAmount; + if (tdsPercent != null) payload["tdsPercent"] = tdsPercent; + if (netPayable != null) payload["netPayable"] = netPayable; + + const endpoint = ApiEndpoints.updateExpenseStatus; + logSafe("Updating expense status with payload: $payload"); + + try { + final response = + await _postRequest(endpoint, payload, customTimeout: extendedTimeout); + + if (response == null) { + logSafe("Update expense status failed: null response", + level: LogLevel.error); + return false; + } + + logSafe("Update expense status response status: ${response.statusCode}"); + logSafe("Update expense status response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Expense status updated successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to update expense status: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + } + } catch (e, stack) { + logSafe("Exception during updateExpenseStatus API: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + static Future?> getExpenseListApi({ + String? filter, + int pageSize = 20, + int pageNumber = 1, + }) async { + String endpoint = ApiEndpoints.getExpenseList; + final queryParams = { + 'pageSize': pageSize.toString(), + 'pageNumber': pageNumber.toString(), + }; + + if (filter?.isNotEmpty ?? false) { + queryParams['filter'] = filter!; + } + + final uri = Uri.parse(endpoint).replace(queryParameters: queryParams); + logSafe("Fetching expense list with URI: $uri"); + + try { + final response = await _getRequest(uri.toString()); + if (response == null) { + logSafe("Expense list request failed: null response", + level: LogLevel.error); + return null; // real failure + } + + final body = response.body.trim(); + if (body.isEmpty) { + logSafe("Expense list response body is empty", level: LogLevel.warning); + return { + "status": true, + "data": {"data": [], "totalPages": 0, "currentPage": pageNumber} + }; // treat as empty list + } + + final jsonResponse = jsonDecode(body); + if (jsonResponse is Map) { + logSafe("Expense list response parsed successfully"); + return jsonResponse; // always return valid JSON, even if data list is empty + } else { + logSafe("Unexpected response structure: $jsonResponse", + level: LogLevel.error); + return null; + } + } catch (e, stack) { + logSafe("Exception during getExpenseListApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return null; + } + } + + static Future> getAdvancePayments( + String employeeId) async { + try { + final endpoint = "${ApiEndpoints.getAdvancePayments}/$employeeId"; + + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("❌ getAdvancePayments: Null response"); + return []; + } + + if (response.statusCode == 200) { + // 🟢 Added log to inspect raw JSON + logSafe("🔍 AdvancePayment raw response: ${response.body}"); + + final Map body = jsonDecode(response.body); + final data = body['data'] ?? body; + return AdvancePayment.listFromJson(data); + } else { + logSafe("⚠ getAdvancePayments failed → ${response.statusCode}"); + return []; + } + } catch (e, s) { + logSafe("❌ ApiService.getAdvancePayments error: $e\n$s", + level: LogLevel.error); + return []; + } + } + + /// Fetch employees with optional query. Returns raw list (List) + static Future> getEmployees( + {Map? queryParams}) async { + try { + final endpoint = ApiEndpoints.getEmployeesWithoutPermission; + + final resp = await _getRequest(endpoint, queryParams: queryParams); + if (resp == null) return []; + + final body = jsonDecode(resp.body); + if (body is Map && body.containsKey('data')) { + final data = body['data']; + if (data is List) return data; + return []; + } else if (body is List) { + return body; + } else { + return []; + } + } catch (e, s) { + logSafe("❌ ApiService.getEmployees error: $e\n$s", level: LogLevel.error); + return []; + } + } + + /// Fetch Master Payment Modes + static Future?> getMasterPaymentModes() async { + const endpoint = ApiEndpoints.getMasterPaymentModes; + return _getRequest(endpoint).then((res) => res != null + ? _parseResponse(res, label: 'Master Payment Modes') + : null); + } + + /// Fetch Master Expense Status + static Future?> getMasterExpenseStatus() async { + const endpoint = ApiEndpoints.getMasterExpenseStatus; + return _getRequest(endpoint).then((res) => res != null + ? _parseResponse(res, label: 'Master Expense Status') + : null); + } + + /// Fetch Master Expense Categorys + static Future?> getMasterExpenseTypes() async { + const endpoint = ApiEndpoints.getMasterExpenseCategory; + return _getRequest(endpoint).then((res) => res != null + ? _parseResponse(res, label: 'Master Expense Categorys') + : null); + } + + /// Create Expense API + 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, + }; + + const endpoint = ApiEndpoints.createExpense; + logSafe("Creating expense with payload: $payload"); + + try { + final response = + await _postRequest(endpoint, payload, customTimeout: extendedTimeout); + + if (response == null) { + logSafe("Create expense failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Create expense response status: ${response.statusCode}"); + logSafe("Create expense response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Expense created successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to create expense: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during createExpense API: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + // === Dashboard Endpoints === + /// Get Dashboard Tasks + static Future getDashboardTasks( + {required String projectId}) async { + try { + final queryParams = {'projectId': projectId}; + + final response = await _getRequest(ApiEndpoints.getDashboardTasks, + queryParams: queryParams); + + if (response == null || response.body.trim().isEmpty) { + logSafe("Dashboard tasks request failed or response empty", + level: LogLevel.error); + return null; + } + + final jsonResponse = jsonDecode(response.body); + if (jsonResponse is Map && + jsonResponse['success'] == true) { + logSafe( + "Dashboard tasks fetched successfully: ${jsonResponse['data']}"); + return DashboardTasks.fromJson(jsonResponse); + } else { + logSafe( + "Failed to fetch dashboard tasks: ${jsonResponse['message'] ?? 'Unknown error'}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during getDashboardTasks API: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + return null; + } + + /// Get Dashboard Teams + static Future getDashboardTeams( + {required String projectId}) async { + try { + final queryParams = {'projectId': projectId}; + + final response = await _getRequest(ApiEndpoints.getDashboardTeams, + queryParams: queryParams); + + if (response == null || response.body.trim().isEmpty) { + logSafe("Dashboard teams request failed or response empty", + level: LogLevel.error); + return null; + } + + final jsonResponse = jsonDecode(response.body); + if (jsonResponse is Map && + jsonResponse['success'] == true) { + logSafe( + "Dashboard teams fetched successfully: ${jsonResponse['data']}"); + return DashboardTeams.fromJson(jsonResponse); + } else { + logSafe( + "Failed to fetch dashboard teams: ${jsonResponse['message'] ?? 'Unknown error'}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during getDashboardTeams API: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + return null; + } + + 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?days=$days"; + + return _getRequest(endpoint).then((res) => res != null + ? _parseResponse(res, label: 'Dashboard Attendance Overview') + : null); + } + + /// Fetch Project Progress + static Future getProjectProgress({ + required String projectId, + required int days, + DateTime? fromDate, // make optional + }) async { + const endpoint = ApiEndpoints.getDashboardProjectProgress; + + // Use today's date if fromDate is not provided + final actualFromDate = fromDate ?? DateTime.now(); + + final queryParams = { + "projectId": projectId, + "days": days.toString(), + "FromDate": + DateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS").format(actualFromDate), + }; + + try { + final response = await _getRequest(endpoint, queryParams: queryParams); + + if (response == null) { + logSafe( + "Project Progress request failed: null response", + level: LogLevel.error, + ); + return null; + } + + final parsed = + _parseResponseForAllData(response, label: "ProjectProgress"); + if (parsed != null) { + logSafe("✅ Project progress fetched successfully"); + return ProjectResponse.fromJson(parsed); + } + } catch (e, stack) { + logSafe("Exception during getProjectProgress: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + /// Directory calling the API + + static Future deleteBucket(String id) async { + final endpoint = "${ApiEndpoints.updateBucket}/$id"; + + try { + final token = await _getToken(); + if (token == null) { + logSafe("Token is null. Cannot proceed with DELETE request.", + level: LogLevel.error); + return false; + } + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); + + logSafe("Sending DELETE request to $uri", level: LogLevel.debug); + + final response = await http + .delete(uri, headers: _headers(token)) + .timeout(extendedTimeout); + + logSafe("DELETE bucket response status: ${response.statusCode}"); + logSafe("DELETE bucket response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) { + logSafe("Bucket deleted successfully."); + return true; + } else { + logSafe( + "Failed to delete bucket: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during deleteBucket API: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + 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"; + + logSafe("Updating bucket with payload: $payload"); + + try { + final response = await _putRequest(endpoint, payload); + + if (response == null) { + logSafe("Update bucket failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Update bucket response status: ${response.statusCode}"); + logSafe("Update bucket response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Bucket updated successfully: ${json['data']}"); + return true; + } else { + logSafe("Failed to update bucket: ${json['message']}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during updateBucket API: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + /// Assign employees to a bucket + static Future assignEmployeesToBucket({ + required String bucketId, + required List> employees, + }) async { + final endpoint = "${ApiEndpoints.assignBucket}/$bucketId"; + + logSafe("Assigning employees to bucket $bucketId: $employees"); + + try { + final response = await _postRequest(endpoint, employees); + + if (response == null) { + logSafe("Assign employees failed: null response", + level: LogLevel.error); + return false; + } + + logSafe("Assign employees response status: ${response.statusCode}"); + logSafe("Assign employees response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Employees assigned successfully"); + return true; + } else { + logSafe("Failed to assign employees: ${json['message']}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during assignEmployeesToBucket API: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + static Future createBucket({ + required String name, + required String description, + }) async { + final payload = { + "name": name, + "description": description, + }; + + final endpoint = ApiEndpoints.createBucket; + + logSafe("Creating bucket with payload: $payload"); + + try { + final response = await _postRequest(endpoint, payload); + + if (response == null) { + logSafe("Create bucket failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Create bucket response status: ${response.statusCode}"); + logSafe("Create bucket response body: ${response.body}"); + + final json = jsonDecode(response.body); + + if (json['success'] == true) { + logSafe("Bucket created successfully: ${json['data']}"); + return true; + } else { + logSafe("Failed to create bucket: ${json['message']}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during createBucket API: ${e.toString()}", + level: LogLevel.error); + logSafe("StackTrace: ${stack.toString()}", level: LogLevel.debug); + } + + return false; + } + + static Future?> getDirectoryNotes({ + int pageSize = 1000, + int pageNumber = 1, + }) async { + final queryParams = { + 'pageSize': pageSize.toString(), + 'pageNumber': pageNumber.toString(), + }; + + final response = await _getRequest( + ApiEndpoints.getDirectoryNotes, + queryParams: queryParams, + ); + + final data = response != null + ? _parseResponse(response, label: 'Directory Notes') + : null; + + return data is Map ? data : null; + } + + static Future addContactComment(String note, String contactId) async { + final payload = { + "note": note, + "contactId": contactId, + }; + + final endpoint = ApiEndpoints.updateDirectoryNotes; + + logSafe("Adding new comment with payload: $payload"); + logSafe("Sending add comment request to $endpoint"); + + try { + final response = await _postRequest(endpoint, payload); + + if (response == null) { + logSafe("Add comment failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Add comment response status: ${response.statusCode}"); + logSafe("Add comment response body: ${response.body}"); + + final json = jsonDecode(response.body); + + if (json['success'] == true) { + logSafe("Comment added successfully for contactId: $contactId"); + return true; + } else { + logSafe("Failed to add comment: ${json['message']}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during addComment API: ${e.toString()}", + level: LogLevel.error); + logSafe("StackTrace: ${stack.toString()}", level: LogLevel.debug); + } + + return false; + } + + /// Get list of assigned projects for a specific employee + /// Get list of assigned projects for a specific employee + static Future?> getAssignedProjects(String employeeId) async { + if (employeeId.isEmpty) { + throw ArgumentError("employeeId must not be empty"); + } + + final endpoint = "${ApiEndpoints.getAssignedProjects}/$employeeId"; + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Failed to fetch assigned projects: null response", + level: LogLevel.error); + return null; + } + + final parsed = _parseResponse(response, label: "Assigned Projects"); + if (parsed is List) { + return parsed; + } else { + logSafe("Unexpected response format for assigned projects.", + level: LogLevel.error); + return null; + } + } catch (e, stack) { + logSafe("Exception during getAssignedProjects API: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return null; + } + } + + /// Assign projects to a specific employee + static Future assignProjects({ + required String employeeId, + required List> projects, + }) async { + if (employeeId.isEmpty) { + throw ArgumentError("employeeId must not be empty"); + } + if (projects.isEmpty) { + throw ArgumentError("projects list must not be empty"); + } + + final endpoint = "${ApiEndpoints.assignProjects}/$employeeId"; + + logSafe("Assigning projects to employee $employeeId: $projects"); + + try { + final response = await _postRequest(endpoint, projects); + + if (response == null) { + logSafe("Assign projects failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Assign projects response status: ${response.statusCode}"); + logSafe("Assign projects response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Projects assigned successfully"); + return true; + } else { + logSafe("Failed to assign projects: ${json['message']}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during assignProjects API: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + + 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, + }; + + logSafe("Updating comment with payload: $payload"); + logSafe("Headers for update comment: $headers"); + logSafe("Sending update comment request to $endpoint"); + + try { + final response = await _putRequest( + endpoint, + payload, + additionalHeaders: headers, + ); + + if (response == null) { + logSafe("Update comment failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Update comment response status: ${response.statusCode}"); + logSafe("Update comment response body: ${response.body}"); + + final json = jsonDecode(response.body); + + if (json['success'] == true) { + logSafe("Comment updated successfully. commentId: $commentId"); + return true; + } else { + logSafe("Failed to update comment: ${json['message']}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during updateComment API: ${e.toString()}", + level: LogLevel.error); + logSafe("StackTrace: ${stack.toString()}", level: LogLevel.debug); + } + + return false; + } + + static Future restoreContactComment( + String commentId, + bool isActive, + ) async { + final endpoint = + "${ApiEndpoints.updateDirectoryNotes}/$commentId?active=$isActive"; + + logSafe( + "Updating comment active status. commentId: $commentId, isActive: $isActive"); + logSafe("Sending request to $endpoint "); + + try { + final response = await _deleteRequest( + endpoint, + ); + + if (response == null) { + logSafe("Update comment failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Update comment response status: ${response.statusCode}"); + logSafe("Update comment response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe( + "Comment active status updated successfully. commentId: $commentId"); + return true; + } else { + logSafe("Failed to update comment: ${json['message']}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during updateComment API: ${e.toString()}", + level: LogLevel.error); + logSafe("StackTrace: ${stack.toString()}", level: LogLevel.debug); + } + + return false; + } + + static Future?> getDirectoryComments( + String contactId, { + bool active = true, + }) async { + final url = "${ApiEndpoints.getDirectoryNotes}/$contactId?active=$active"; + final response = await _getRequest(url); + final data = response != null + ? _parseResponse(response, label: 'Directory Comments') + : null; + + return data is List ? data : null; + } + + /// Deletes a directory contact (sets active=false) + static Future deleteDirectoryContact(String contactId) async { + final endpoint = "${ApiEndpoints.updateContact}/$contactId/"; + final queryParams = {'active': 'false'}; + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") + .replace(queryParameters: queryParams); + + _log("Deleting directory contact at $uri"); + + final response = await _deleteRequest( + "$endpoint?active=false", + ); + + if (response != null && response.statusCode == 200) { + _log("Contact deleted successfully: ${response.body}"); + return true; + } + + _log("Failed to delete contact: ${response?.body}"); + return false; + } + + /// Restores a directory contact (sets active=true) + static Future restoreDirectoryContact(String contactId) async { + final endpoint = "${ApiEndpoints.updateContact}/$contactId/"; + final queryParams = {'active': 'true'}; + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") + .replace(queryParameters: queryParams); + + _log("Restoring directory contact at $uri"); + + final response = await _deleteRequest( + "$endpoint?active=true", + ); + + if (response != null && response.statusCode == 200) { + _log("Contact restored successfully: ${response.body}"); + return true; + } + + _log("Failed to restore contact: ${response?.body}"); + return false; + } + + static Future updateContact( + String contactId, Map payload) async { + try { + final endpoint = "${ApiEndpoints.updateContact}/$contactId"; + + logSafe("Updating contact [$contactId] with payload: $payload"); + + final response = await _putRequest(endpoint, payload); + if (response != null) { + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Contact updated successfully."); + return true; + } else { + logSafe("Update contact failed: ${json['message']}", + level: LogLevel.warning); + } + } + } catch (e) { + logSafe("Error updating contact: $e", level: LogLevel.error); + } + return false; + } + + static Future createContact(Map payload) async { + try { + logSafe("Submitting contact payload: $payload"); + + final response = await _postRequest(ApiEndpoints.createContact, payload); + if (response != null) { + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Contact created successfully."); + return true; + } else { + logSafe("Create contact failed: ${json['message']}", + level: LogLevel.warning); + } + } + } catch (e) { + logSafe("Error creating contact: $e", level: LogLevel.error); + } + return false; + } + + static Future> getOrganizationList() async { + try { + final url = ApiEndpoints.getDirectoryOrganization; + logSafe("Sending GET request to: $url", level: LogLevel.info); + + final response = await _getRequest(url); + + logSafe("Response status: ${response?.statusCode}", + level: LogLevel.debug); + logSafe("Response body: ${response?.body}", level: LogLevel.debug); + + if (response != null && response.statusCode == 200) { + final body = jsonDecode(response.body); + if (body['success'] == true && body['data'] is List) { + return List.from(body['data']); + } + } + } catch (e, stackTrace) { + logSafe("Failed to fetch organization names: $e", level: LogLevel.error); + logSafe("Stack trace: $stackTrace", level: LogLevel.debug); + } + return []; + } + + static Future?> getContactCategoryList() async => + _getRequest(ApiEndpoints.getDirectoryContactCategory).then((res) => + res != null + ? _parseResponseForAllData(res, label: 'Contact Category List') + : null); + + static Future?> getContactTagList() async => + _getRequest(ApiEndpoints.getDirectoryContactTags).then((res) => + res != null + ? _parseResponseForAllData(res, label: 'Contact Tag List') + : null); + + static Future?> getDirectoryData( + {required bool isActive}) async { + final queryParams = { + "active": isActive.toString(), + }; + + return _getRequest(ApiEndpoints.getDirectoryContacts, + queryParams: queryParams) + .then((res) => + res != null ? _parseResponse(res, label: 'Directory Data') : null); + } + + static Future?> getContactBucketList() async => + _getRequest(ApiEndpoints.getDirectoryBucketList).then((res) => res != null + ? _parseResponseForAllData(res, label: 'Contact Bucket List') + : null); + + // === Attendance APIs === + + static Future?> getProjects() async => + _getRequest(ApiEndpoints.getProjects).then( + (res) => res != null ? _parseResponse(res, label: 'Projects') : null); + + static Future?> getGlobalProjects() async => + _getRequest(ApiEndpoints.getGlobalProjects).then((res) => + res != null ? _parseResponse(res, label: 'Global Projects') : null); + + static Future?> getTodaysAttendance( + String projectId, { + String? organizationId, + }) async { + final query = { + "projectId": projectId, + if (organizationId != null) "organizationId": organizationId, + }; + + return _getRequest(ApiEndpoints.getTodaysAttendance, queryParams: query) + .then((res) => + res != null ? _parseResponse(res, label: 'Employees') : null); + } + + static Future?> getAttendanceForDashboard( + String projectId) async { + String endpoint = ApiEndpoints.getAttendanceForDashboard.replaceFirst( + ':projectId', + projectId, + ); + + final res = await _getRequest(endpoint); + + if (res == null) return null; + + final data = _parseResponse(res, label: 'Dashboard Attendance'); + if (data == null) return null; + + // Wrap single object in a list if needed + 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, + }; + + return _getRequest(ApiEndpoints.getRegularizationLogs, queryParams: query) + .then((res) => res != null + ? _parseResponse(res, label: 'Regularization Logs') + : null); + } + + 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, + }; + + return _getRequest(ApiEndpoints.getAttendanceLogs, queryParams: query).then( + (res) => + res != null ? _parseResponse(res, label: 'Attendance Logs') : null); + } + + static Future?> getAttendanceLogView(String id) async => + _getRequest("${ApiEndpoints.getAttendanceLogView}/$id").then((res) => + res != null ? _parseResponse(res, label: 'Log Details') : null); + + 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, // 👈 now required + required String date, // 👈 new required param + }) async { + final body = { + "id": id, + "employeeId": employeeId, + "projectId": projectId, + "markTime": markTime, // 👈 directly from UI + "comment": comment, + "action": action, + "date": date, // 👈 directly from UI + 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) { + logSafe("Image encoding error: $e", level: LogLevel.error); + return false; + } + } + + final response = await _postRequest( + ApiEndpoints.uploadAttendanceImage, + body, + customTimeout: extendedTimeout, + ); + + if (response == null) return false; + + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) return true; + + logSafe("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); + return false; + } + + 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"; + } + + // === Employee APIs === + /// Search employees by first name and last name only (not middle name) + /// Returns a list of up to 10 employee records matching the search string. + static Future?> searchEmployeesBasic({ + String? searchString, + }) async { + // Remove ArgumentError check because searchString is optional now + + final queryParams = {}; + + // Add searchString to query parameters only if it's not null or empty + if (searchString != null && searchString.isNotEmpty) { + queryParams['searchString'] = searchString; + } + + final response = await _getRequest( + ApiEndpoints.getEmployeesWithoutPermission, + queryParams: queryParams, + ); + + if (response != null) { + return _parseResponse(response, label: 'Search Employees Basic'); + } + + return null; + } + + static Future?> getAllEmployeesByProject(String projectId, + {String? organizationId}) async { + if (projectId.isEmpty) throw ArgumentError('projectId must not be empty'); + + // Build the endpoint with optional organizationId query + var endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId"; + if (organizationId != null && organizationId.isNotEmpty) { + endpoint += "?organizationId=$organizationId"; + } + + return _getRequest(endpoint).then( + (res) => res != null + ? _parseResponse(res, label: 'Employees by Project') + : null, + ); + } + + /// Fetches employees by projectId, serviceId, and organizationId + static Future?> getEmployeesByProjectService( + String projectId, { + String? serviceId, + String? organizationId, + }) async { + if (projectId.isEmpty) { + throw ArgumentError('projectId must not be empty'); + } + + // Construct query parameters only if non-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 _getRequest(endpoint, queryParams: queryParams); + + if (response != null) { + return _parseResponse(response, label: 'Employees by Project Service'); + } else { + return null; + } + } + + static Future?> getAllEmployees( + {String? organizationId}) async { + var endpoint = ApiEndpoints.getAllEmployees; + + // Add organization filter if provided + if (organizationId != null && organizationId.isNotEmpty) { + endpoint += "?organizationId=$organizationId"; + } + + return _getRequest(endpoint).then( + (res) => res != null ? _parseResponse(res, label: 'All Employees') : null, + ); + } + + static Future?> getRoles() async => + _getRequest(ApiEndpoints.getRoles).then( + (res) => res != null ? _parseResponse(res, label: 'Roles') : null); + 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 _postRequest( + ApiEndpoints.createEmployee, + body, + customTimeout: extendedTimeout, + ); + + if (response == null) return null; + + final json = jsonDecode(response.body); + return { + "success": response.statusCode == 200 && json['success'] == true, + "data": json, + }; + } + + static Future?> getEmployeeDetails( + String employeeId) async { + final url = "${ApiEndpoints.getEmployeeInfo}/$employeeId"; + final response = await _getRequest(url); + final data = response != null + ? _parseResponse(response, label: 'Employee Details') + : null; + return data is Map ? data : null; + } + + // === Daily Task APIs === + /// Get Daily Task Project Report Filter + static Future getDailyTaskFilter( + String projectId) async { + final endpoint = + "${ApiEndpoints.getDailyTaskProjectProgressFilter}/$projectId"; + logSafe("Fetching daily task Progress filter for projectId: $projectId"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Daily task filter request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData(response, + label: "Daily Task Progress Filter"); + + if (jsonResponse != null) { + return DailyProgressReportFilterResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getDailyTask Progress Filter: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + + static Future?> getDailyTasks( + String projectId, { + Map? filter, + int pageNumber = 1, + int pageSize = 20, + }) async { + // Build query parameters + final query = { + "projectId": projectId, + "pageNumber": pageNumber.toString(), + "pageSize": pageSize.toString(), + if (filter != null) "filter": jsonEncode(filter), + }; + + final uri = + Uri.parse(ApiEndpoints.getDailyTask).replace(queryParameters: query); + + final response = await _getRequest(uri.toString()); + final parsed = response != null + ? _parseResponse(response, label: 'Daily Tasks') + : null; + + if (parsed != null && parsed['data'] != null) { + 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 _postRequest( + ApiEndpoints.reportTask, + body, + customTimeout: extendedTimeout, + ); + + if (response == null) return false; + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) { + Get.back(); + return true; + } + logSafe("Failed to report task: ${json['message'] ?? 'Unknown error'}"); + 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 _postRequest(ApiEndpoints.commentTask, body); + if (response == null) return false; + final json = jsonDecode(response.body); + return response.statusCode == 200 && json['success'] == true; + } + + /// Fetch infra details for a project, optionally filtered by service + static Future?> getInfraDetails(String projectId, + {String? serviceId}) async { + String endpoint = "/project/infra-details/$projectId"; + + if (serviceId != null && serviceId.isNotEmpty) { + endpoint += "?serviceId=$serviceId"; + } + + final res = await _getRequest(endpoint); + if (res == null) { + logSafe('Infra Details API returned null'); + return null; + } + + logSafe('Infra Details raw response: ${res.body}'); + return _parseResponseForAllData(res, label: 'Infra Details') + as Map?; + } + + /// Fetch work items for a given work area, optionally filtered by service + static Future?> getWorkItemsByWorkArea(String workAreaId, + {String? serviceId}) async { + String endpoint = "/project/tasks/$workAreaId"; + + if (serviceId != null && serviceId.isNotEmpty) { + endpoint += "?serviceId=$serviceId"; + } + + final res = await _getRequest(endpoint); + if (res == null) { + logSafe('Work Items API returned null'); + return null; + } + + logSafe('Work Items raw response: ${res.body}'); + return _parseResponseForAllData(res, label: 'Work Items') + 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 _postRequest(ApiEndpoints.assignDailyTask, body); + if (response == null) return false; + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) { + Get.back(); + return true; + } + logSafe( + "Failed to assign daily task: ${json['message'] ?? 'Unknown error'}"); + return false; + } + + static Future?> getWorkStatus() async { + final res = await _getRequest(ApiEndpoints.getWorkStatus); + if (res == null) { + logSafe('Work Status API returned null'); + return null; + } + + logSafe('Work Status raw response: ${res.body}'); + return _parseResponseForAllData(res, label: 'Work Status') + as Map?; + } + + static Future?> getMasterWorkCategories() async => + _getRequest(ApiEndpoints.getmasterWorkCategories).then((res) => + res != null + ? _parseResponseForAllData(res, label: 'Master Work Categories') + : null); + + 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 _postRequest(ApiEndpoints.approveReportAction, body); + if (response == null) return false; + + final json = jsonDecode(response.body); + return response.statusCode == 200 && json['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 _postRequest(ApiEndpoints.assignTask, body); + if (response == null) return false; + + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) { + Get.back(); + return true; + } + + logSafe("Failed to create task: ${json['message'] ?? 'Unknown error'}"); + return false; + } +} diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 6f60a02..dbf41d2 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -47,7 +47,7 @@ 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'; - +import 'package:on_field_work/helpers/utils/encryption_helper.dart'; class ApiService { static const bool enableLogs = true; @@ -112,37 +112,70 @@ class ApiService { } static dynamic _parseResponse(http.Response response, {String label = ''}) { - _log("$label Response: ${response.body}"); - try { - final json = jsonDecode(response.body); - if (response.statusCode == 200 && json['success'] == true) { - return json['data']; - } - _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); - } catch (e) { - _log("Response parsing error [$label]: $e"); + _log("$label Encrypted Response: ${response.body}"); // Log encrypted body + + // --- ⚠️ START of Decryption Change ⚠️ --- + final decryptedData = + decryptResponse(response.body); // Decrypt the Base64 string + + if (decryptedData == null) { + _log("Decryption failed for [$label]. Cannot parse response."); + return null; } + // If decryptedData is a Map/List (JSON), use it directly. + // If it's a plain String, you'll need to decode it to JSON. + + // Assuming the decrypted result is a Map/List (JSON), as per your API structure: + final json = decryptedData; + + // Now proceed with your existing logic using the decrypted JSON object + if (response.statusCode == 200 && json is Map && json['success'] == true) { + _log("$label Decrypted Data: ${json['data']}"); + return json['data']; + } + + // Handle error cases using the decrypted data + if (json is Map) { + _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); + } else { + _log("API Error [$label]: Decrypted response not a map: $json"); + } + + // --- ⚠️ END of Decryption Change ⚠️ --- return null; } static dynamic _parseResponseForAllData(http.Response response, {String label = ''}) { - _log("$label Response: ${response.body}"); + _log("$label Encrypted Response: ${response.body}"); - try { - final body = response.body.trim(); - if (body.isEmpty) throw FormatException("Empty response body"); - - final json = jsonDecode(body); - if (response.statusCode == 200 && json['success'] == true) { - return json; - } - - _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); - } catch (e) { - _log("Response parsing error [$label]: $e"); + // --- ⚠️ START of Decryption Change ⚠️ --- + final body = response.body.trim(); + if (body.isEmpty) { + _log("Empty response body for [$label]"); + return null; } + final json = decryptResponse(body); // Decrypt and auto-decode JSON + + if (json == null) { + _log("Decryption failed for [$label]. Cannot parse response."); + return null; + } + + if (json is Map && response.statusCode == 200 && json['success'] == true) { + _log("$label Decrypted JSON: $json"); + return json; // Return the full JSON map + } + + // Handle error cases + if (json is Map) { + _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); + } else { + _log("API Error [$label]: Decrypted response not a map: $json"); + } + + // --- ⚠️ END of Decryption Change ⚠️ --- return null; } @@ -319,8 +352,7 @@ class ApiService { } } - - /// ============================================ + /// ============================================ /// GET PURCHASE INVOICE OVERVIEW (Dashboard) /// ============================================ static Future getPurchaseInvoiceOverview({ @@ -357,6 +389,7 @@ class ApiService { return null; } } + /// ============================================ /// GET COLLECTION OVERVIEW (Dashboard) /// ============================================ @@ -1836,6 +1869,7 @@ class ApiService { 'Authorization': 'Bearer $token', }; + // Send logs as JSON final response = await http .post(uri, headers: headers, body: jsonEncode(logs)) .timeout(ApiService.extendedTimeout); @@ -1843,15 +1877,28 @@ class ApiService { logSafe("Post logs response status: ${response.statusCode}"); logSafe("Post logs response body: ${response.body}"); - if (response.statusCode == 200 && response.body.isNotEmpty) { - final json = jsonDecode(response.body); - if (json['success'] == true) { - logSafe("Logs posted successfully."); - return true; - } + // --- Decrypt response before parsing --- + final decryptedData = + decryptResponse(response.body); // returns Map/List or null + + if (decryptedData == null) { + logSafe("Decryption failed. Cannot parse logs response.", + level: LogLevel.error); + return false; } - logSafe("Failed to post logs: ${response.body}", level: LogLevel.warning); + // Expecting decrypted data to be a Map with 'success' field + if (response.statusCode == 200 && + decryptedData is Map && + decryptedData['success'] == true) { + logSafe("Logs posted successfully."); + return true; + } else { + final errorMsg = decryptedData is Map + ? decryptedData['message'] ?? 'Unknown error' + : 'Decrypted response not a Map: $decryptedData'; + logSafe("Failed to post logs: $errorMsg", level: LogLevel.warning); + } } catch (e, stack) { logSafe("Exception during postLogsApi: $e", level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); diff --git a/lib/helpers/services/auth_service.dart b/lib/helpers/services/auth_service.dart index 7582157..04520e1 100644 --- a/lib/helpers/services/auth_service.dart +++ b/lib/helpers/services/auth_service.dart @@ -3,6 +3,7 @@ import 'package:http/http.dart' as http; import 'package:on_field_work/helpers/services/api_endpoints.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/app_logger.dart'; +import 'package:on_field_work/helpers/utils/encryption_helper.dart'; class AuthService { static const String _baseUrl = ApiEndpoints.baseUrl; @@ -11,16 +12,13 @@ class AuthService { }; static bool isLoggedIn = false; - /* -------------------------------------------------------------------------- */ - /* Logout API */ - /* -------------------------------------------------------------------------- */ + +/* -------------------------------------------------------------------------- / +/ Logout API / +/ -------------------------------------------------------------------------- */ static Future logoutApi(String refreshToken, String fcmToken) async { try { - final body = { - "refreshToken": refreshToken, - "fcmToken": fcmToken, - }; - + final body = {"refreshToken": refreshToken, "fcmToken": fcmToken}; final response = await _post("/auth/logout", body); if (response != null && response['statusCode'] == 200) { @@ -37,9 +35,9 @@ class AuthService { } } - /* -------------------------------------------------------------------------- */ - /* Public Methods */ - /* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- / +/ Public Methods / +/ -------------------------------------------------------------------------- */ static Future registerDeviceToken(String fcmToken) async { final token = await LocalStorage.getJwtToken(); @@ -50,18 +48,6 @@ class AuthService { } final body = {"fcmToken": fcmToken}; - final headers = { - ..._headers, - 'Authorization': 'Bearer $token', - }; - final endpoint = "$_baseUrl/auth/set/device-token"; - - // 🔹 Log request details - logSafe("📡 Device Token API Request"); - logSafe("➡️ Endpoint: $endpoint"); - logSafe("➡️ Headers: ${jsonEncode(headers)}"); - logSafe("➡️ Payload: ${jsonEncode(body)}"); - final data = await _post("/auth/set/device-token", body, authToken: token); if (data != null && data['success'] == true) { @@ -76,9 +62,6 @@ class AuthService { static Future?> loginUser( Map data) async { logSafe("Attempting login..."); - logSafe("Login payload (raw): $data"); - logSafe("Login payload (JSON): ${jsonEncode(data)}"); - final responseData = await _post("/auth/app/login", data); if (responseData == null) return {"error": "Network error. Please check your connection."}; @@ -94,8 +77,8 @@ class AuthService { } static Future refreshToken() async { - final accessToken = LocalStorage.getJwtToken(); - final refreshToken = LocalStorage.getRefreshToken(); + final accessToken = LocalStorage.getJwtToken(); + final refreshToken = LocalStorage.getRefreshToken(); if ([accessToken, refreshToken].any((t) => t == null || t.isEmpty)) { logSafe("Missing access or refresh token.", level: LogLevel.warning); @@ -110,17 +93,10 @@ class AuthService { await LocalStorage.setLoggedInUser(true); logSafe("Token refreshed successfully."); - // 🔹 Retry FCM token registration after token refresh - final newFcmToken = LocalStorage.getFcmToken(); + final newFcmToken = LocalStorage.getFcmToken(); if (newFcmToken?.isNotEmpty ?? false) { - final success = await registerDeviceToken(newFcmToken!); - logSafe( - success - ? "✅ FCM token re-registered after JWT refresh." - : "⚠️ Failed to register FCM token after JWT refresh.", - level: success ? LogLevel.info : LogLevel.warning); + await registerDeviceToken(newFcmToken!); } - return true; } logSafe("Refresh token failed: ${data?['message']}", @@ -223,10 +199,13 @@ class AuthService { }; final response = await http.post(Uri.parse("$_baseUrl$path"), headers: headers, body: jsonEncode(body)); - return { - ...jsonDecode(response.body), - "statusCode": response.statusCode, - }; + + final decrypted = decryptResponse(response.body); // <-- Decrypt here + if (decrypted is Map) { + return {"statusCode": response.statusCode, ...decrypted}; + } else { + return {"statusCode": response.statusCode, "data": decrypted}; + } } catch (e, st) { _handleError("$path POST error", e, st); return null; @@ -245,10 +224,13 @@ class AuthService { }; final response = await http.get(Uri.parse("$_baseUrl$path"), headers: headers); - return { - ...jsonDecode(response.body), - "statusCode": response.statusCode, - }; + + final decrypted = decryptResponse(response.body); // <-- Decrypt here + if (decrypted is Map) { + return {"statusCode": response.statusCode, ...decrypted}; + } else { + return {"statusCode": response.statusCode, "data": decrypted}; + } } catch (e, st) { _handleError("$path GET error", e, st); return null; @@ -270,8 +252,6 @@ class AuthService { } static Future _handleLoginSuccess(Map data) async { - logSafe("Processing login success..."); - await LocalStorage.setJwtToken(data['token']); await LocalStorage.setLoggedInUser(true); @@ -287,6 +267,5 @@ class AuthService { await LocalStorage.removeMpinToken(); } isLoggedIn = true; - logSafe("✅ Login flow completed and controllers initialized."); } } diff --git a/lib/helpers/services/http_client.dart b/lib/helpers/services/http_client.dart new file mode 100644 index 0000000..bed72df --- /dev/null +++ b/lib/helpers/services/http_client.dart @@ -0,0 +1,255 @@ +// lib/helpers/services/http_client.dart + +import 'dart:convert'; +import 'dart:io'; +import 'package:http/http.dart' as http; +import 'package:jwt_decoder/jwt_decoder.dart'; +import 'package:on_field_work/helpers/services/app_logger.dart'; +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:on_field_work/helpers/utils/encryption_helper.dart'; + +/// Centralized HTTP client with automatic token management, encryption, +/// and retry logic for OnFieldWork.com API communication. +class HttpClient { + static const Duration _timeout = Duration(seconds: 60); + static const Duration _tokenRefreshThreshold = Duration(minutes: 2); + + final http.Client _client = http.Client(); + bool _isRefreshing = false; + + /// Private constructor - use singleton instance + HttpClient._(); + static final HttpClient instance = HttpClient._(); + + /// Clean headers with JWT token + Map _defaultHeaders(String token) => { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + /// Ensures valid token with proactive refresh + Future _getValidToken() async { + String? token = await LocalStorage.getJwtToken(); + + if (token == null) { + logSafe("No JWT token available", level: LogLevel.error); + await LocalStorage.logout(); + return null; + } + + try { + if (JwtDecoder.isExpired(token) || + JwtDecoder.getExpirationDate(token).difference(DateTime.now()) < + _tokenRefreshThreshold) { + logSafe("Token expired/expiring soon. Refreshing...", + level: LogLevel.info); + if (!await _refreshTokenIfPossible()) { + logSafe("Token refresh failed. Logging out.", level: LogLevel.error); + await LocalStorage.logout(); + return null; + } + token = await LocalStorage.getJwtToken(); + } + } catch (e) { + logSafe("Token validation failed: $e. Logging out.", + level: LogLevel.error); + await LocalStorage.logout(); + return null; + } + + return token; + } + + /// Attempts token refresh with concurrency protection + Future _refreshTokenIfPossible() async { + if (_isRefreshing) return false; + + _isRefreshing = true; + try { + return await AuthService.refreshToken(); + } finally { + _isRefreshing = false; + } + } + + /// Unified response parser with decryption and validation + dynamic _parseResponse( + http.Response response, { + required String endpoint, + bool fullResponse = false, + }) { + final body = response.body.trim(); + + if (body.isEmpty && + response.statusCode >= 200 && + response.statusCode < 300) { + logSafe("Empty response for $endpoint - returning default structure", + level: LogLevel.info); + return fullResponse ? {'success': true, 'data': []} : []; + } + + final decryptedData = decryptResponse(body); + if (decryptedData == null) { + logSafe("❌ Decryption failed for $endpoint", level: LogLevel.error); + return null; + } + + final jsonData = decryptedData; + + if (response.statusCode >= 200 && response.statusCode < 300) { + if (jsonData is Map && jsonData['success'] == true) { + logSafe("✅ $endpoint: Success (${response.statusCode})", + level: LogLevel.info); + return fullResponse ? jsonData : jsonData['data']; + } else if (jsonData is Map) { + logSafe( + "⚠️ $endpoint: API error - ${jsonData['message'] ?? 'Unknown error'}", + level: LogLevel.warning); + } + } + + logSafe("❌ $endpoint: HTTP ${response.statusCode} - $jsonData", + level: LogLevel.error); + return null; + } + + /// Generic request executor with 401 retry logic + Future _execute( + String method, + String endpoint, { + Map? queryParams, + Object? body, + Map? extraHeaders, + bool hasRetried = false, + }) async { + final token = await _getValidToken(); + if (token == null) return null; + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint").replace( + queryParameters: + (method == 'GET' || method == 'DELETE') ? queryParams : null); + + final headers = { + ..._defaultHeaders(token), + if (extraHeaders != null) ...extraHeaders, + }; + + final requestBody = body != null ? jsonEncode(body) : null; + logSafe( + "📡 $method $uri${requestBody != null ? ' | Body: ${requestBody.length > 100 ? '${requestBody.substring(0, 100)}...' : requestBody}' : ''}", + level: LogLevel.debug); + + try { + final response = switch (method) { + 'GET' => await _client.get(uri, headers: headers).timeout(_timeout), + 'POST' => await _client + .post(uri, headers: headers, body: requestBody) + .timeout(_timeout), + 'PUT' => await _client + .put(uri, headers: headers, body: requestBody) + .timeout(_timeout), + 'PATCH' => await _client + .patch(uri, headers: headers, body: requestBody) + .timeout(_timeout), + 'DELETE' => + await _client.delete(uri, headers: headers).timeout(_timeout), + _ => throw HttpException('Unsupported method: $method'), + }; + + // Handle 401 with single retry + if (response.statusCode == 401 && !hasRetried) { + logSafe("🔄 401 detected for $endpoint - retrying with fresh token", + level: LogLevel.warning); + if (await _refreshTokenIfPossible()) { + return await _execute(method, endpoint, + queryParams: queryParams, + body: body, + extraHeaders: extraHeaders, + hasRetried: true); + } + await LocalStorage.logout(); + return null; + } + + return response; + } on SocketException catch (e) { + logSafe("🌐 Network error for $endpoint: $e", level: LogLevel.error); + return null; + } catch (e, stackTrace) { + logSafe("💥 HTTP $method error for $endpoint: $e\n$stackTrace", + level: LogLevel.error); + return null; + } + } + + // Public API - Clean and consistent + Future get( + String endpoint, { + Map? queryParams, + bool fullResponse = false, + }) async { + final response = await _execute('GET', endpoint, queryParams: queryParams); + return response != null + ? _parseResponse(response, + endpoint: endpoint, fullResponse: fullResponse) + : null; + } + + Future post( + String endpoint, + Object? body, { + bool fullResponse = false, + }) async { + final response = await _execute('POST', endpoint, body: body); + return response != null + ? _parseResponse(response, + endpoint: endpoint, fullResponse: fullResponse) + : null; + } + + Future put( + String endpoint, + Object? body, { + Map? extraHeaders, + bool fullResponse = false, + }) async { + final response = + await _execute('PUT', endpoint, body: body, extraHeaders: extraHeaders); + return response != null + ? _parseResponse(response, + endpoint: endpoint, fullResponse: fullResponse) + : null; + } + + Future patch( + String endpoint, + Object? body, { + bool fullResponse = false, + }) async { + final response = await _execute('PATCH', endpoint, body: body); + return response != null + ? _parseResponse(response, + endpoint: endpoint, fullResponse: fullResponse) + : null; + } + + Future delete( + String endpoint, { + Map? queryParams, + bool fullResponse = false, + }) async { + final response = + await _execute('DELETE', endpoint, queryParams: queryParams); + return response != null + ? _parseResponse(response, + endpoint: endpoint, fullResponse: fullResponse) + : null; + } + + /// Proper cleanup for long-lived instances + void dispose() { + _client.close(); + } +} diff --git a/lib/helpers/services/permission_service.dart b/lib/helpers/services/permission_service.dart index 0093e05..bab696b 100644 --- a/lib/helpers/services/permission_service.dart +++ b/lib/helpers/services/permission_service.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -9,103 +8,113 @@ import 'package:on_field_work/model/projects_model.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart'; 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/utils/encryption_helper.dart'; class PermissionService { - // In-memory cache keyed by user token - static final Map> _userDataCache = {}; - static const String _baseUrl = ApiEndpoints.baseUrl; +static final Map> _userDataCache = {}; +static const String _baseUrl = ApiEndpoints.baseUrl; - /// Fetches all user-related data (permissions, employee info, projects). - /// Uses in-memory cache for repeated token queries during session. - static Future> fetchAllUserData( - String token, { - bool hasRetried = false, - }) async { - logSafe("Fetching user data..."); +static Future> fetchAllUserData( +String token, { +bool hasRetried = false, +}) async { +logSafe("Fetching user data..."); - // Check for cached data before network request - final cached = _userDataCache[token]; - if (cached != null) { - logSafe("User data cache hit."); - return cached; - } - - final uri = Uri.parse("$_baseUrl/user/profile"); - final headers = {'Authorization': 'Bearer $token'}; - - try { - final response = await http.get(uri, headers: headers); - final statusCode = response.statusCode; - - if (statusCode == 200) { - final raw = json.decode(response.body); - final data = raw['data'] as Map; - - final result = { - 'permissions': _parsePermissions(data['featurePermissions']), - 'employeeInfo': _parseEmployeeInfo(data['employeeInfo']), - 'projects': _parseProjectsInfo(data['projects']), - }; - - _userDataCache[token] = result; // Cache it for future use - logSafe("User data fetched successfully."); - return result; - } - - // Token expired, try refresh once then redirect on failure - if (statusCode == 401 && !hasRetried) { - logSafe("Unauthorized. Attempting token refresh...", level: LogLevel.warning); - - final refreshed = await AuthService.refreshToken(); - if (refreshed) { - final newToken = await LocalStorage.getJwtToken(); - if (newToken != null && newToken.isNotEmpty) { - return fetchAllUserData(newToken, hasRetried: true); - } - } - - await _handleUnauthorized(); - logSafe("Token refresh failed. Redirecting to login.", level: LogLevel.warning); - throw Exception('Unauthorized. Token refresh failed.'); - } - - final errorMsg = json.decode(response.body)['message'] ?? 'Unknown error'; - logSafe("Failed to fetch user data: $errorMsg", level: LogLevel.warning); - throw Exception('Failed to fetch user data: $errorMsg'); - } catch (e, stacktrace) { - logSafe("Exception while fetching user data", level: LogLevel.error, error: e, stackTrace: stacktrace); - rethrow; // Let the caller handle or report - } - } - - /// Handles unauthorized/user sign out flow - static Future _handleUnauthorized() async { - logSafe("Clearing tokens and redirecting to login due to unauthorized access.", level: LogLevel.warning); - await LocalStorage.removeToken('jwt_token'); - await LocalStorage.removeToken('refresh_token'); - await LocalStorage.setLoggedInUser(false); - Get.offAllNamed('/auth/login-option'); - } - - /// Robust model parsing for permissions - static List _parsePermissions(List permissions) { - logSafe("Parsing user permissions..."); - return permissions - .map((perm) => UserPermission.fromJson({'id': perm})) - .toList(); - } - - /// Robust model parsing for employee info - static EmployeeInfo _parseEmployeeInfo(Map? data) { - logSafe("Parsing employee info..."); - if (data == null) throw Exception("Employee data missing"); - return EmployeeInfo.fromJson(data); - } - - /// Robust model parsing for projects list - static List _parseProjectsInfo(List? projects) { - logSafe("Parsing projects info..."); - if (projects == null) return []; - return projects.map((proj) => ProjectInfo.fromJson(proj)).toList(); - } +final cached = _userDataCache[token]; +if (cached != null) { + logSafe("User data cache hit."); + return cached; } + +final uri = Uri.parse("$_baseUrl/user/profile"); +final headers = {'Authorization': 'Bearer $token'}; + +try { + final response = await http.get(uri, headers: headers); + final statusCode = response.statusCode; + + if (response.body.isEmpty || response.body.trim().isEmpty) { + logSafe("❌ Empty user data response — auto logout"); + await _handleUnauthorized(); + throw Exception("Empty user data response"); + } + + final decrypted = decryptResponse(response.body); + if (decrypted == null) { + logSafe("❌ Failed to decrypt user data — auto logout", level: LogLevel.error); + await _handleUnauthorized(); + throw Exception("Decryption failed for user data"); + } + + final data = decrypted is Map ? decrypted['data'] ?? decrypted : null; + if (data == null || data is! Map) { + logSafe("❌ Decrypted user data is invalid — auto logout", level: LogLevel.error); + await _handleUnauthorized(); + throw Exception("Invalid decrypted user data"); + } + + if (statusCode == 200) { + final result = { + 'permissions': _parsePermissions(data['featurePermissions']), + 'employeeInfo': _parseEmployeeInfo(data['employeeInfo']), + 'projects': _parseProjectsInfo(data['projects']), + }; + + _userDataCache[token] = result; + logSafe("User data fetched and decrypted successfully."); + return result; + } + + if (statusCode == 401 && !hasRetried) { + logSafe("Unauthorized. Attempting token refresh...", level: LogLevel.warning); + + final refreshed = await AuthService.refreshToken(); + if (refreshed) { + final newToken = await LocalStorage.getJwtToken(); + if (newToken != null && newToken.isNotEmpty) { + return fetchAllUserData(newToken, hasRetried: true); + } + } + + await _handleUnauthorized(); + logSafe("Token refresh failed. Redirecting to login.", level: LogLevel.warning); + throw Exception('Unauthorized. Token refresh failed.'); + } + + final errorMsg = data['message'] ?? 'Unknown error'; + logSafe("Failed to fetch user data: $errorMsg", level: LogLevel.warning); + throw Exception('Failed to fetch user data: $errorMsg'); +} catch (e, stacktrace) { + logSafe("Exception while fetching user data", level: LogLevel.error, error: e, stackTrace: stacktrace); + rethrow; +} + + +} + +static Future _handleUnauthorized() async { +logSafe("Clearing tokens and redirecting to login due to unauthorized access.", level: LogLevel.warning); +await LocalStorage.removeToken('jwt_token'); +await LocalStorage.removeToken('refresh_token'); +await LocalStorage.setLoggedInUser(false); +Get.offAllNamed('/auth/login-option'); +} + +static List _parsePermissions(List? permissions) { +logSafe("Parsing user permissions..."); +if (permissions == null) return []; +return permissions.map((perm) => UserPermission.fromJson({'id': perm})).toList(); +} + +static EmployeeInfo _parseEmployeeInfo(Map? data) { +logSafe("Parsing employee info..."); +if (data == null) throw Exception("Employee data missing"); +return EmployeeInfo.fromJson(data); +} + +static List _parseProjectsInfo(List? projects) { +logSafe("Parsing projects info..."); +if (projects == null) return []; +return projects.map((proj) => ProjectInfo.fromJson(proj)).toList(); +} +} \ No newline at end of file diff --git a/lib/helpers/services/tenant_service.dart b/lib/helpers/services/tenant_service.dart index 5a574c7..337e823 100644 --- a/lib/helpers/services/tenant_service.dart +++ b/lib/helpers/services/tenant_service.dart @@ -2,172 +2,154 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:get/get.dart'; import 'package:on_field_work/controller/project_controller.dart'; - import 'package:on_field_work/helpers/services/api_endpoints.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:on_field_work/model/tenant/tenant_list_model.dart'; +import 'package:on_field_work/helpers/utils/encryption_helper.dart'; -/// Abstract interface for tenant service functionality abstract class ITenantService { - Future>?> getTenants({bool hasRetried = false}); - Future selectTenant(String tenantId, {bool hasRetried = false}); +Future>?> getTenants({bool hasRetried = false}); +Future selectTenant(String tenantId, {bool hasRetried = false}); } -/// Tenant API service class TenantService implements ITenantService { - static const String _baseUrl = ApiEndpoints.baseUrl; - static const Map _headers = { - 'Content-Type': 'application/json', - }; +static const String _baseUrl = ApiEndpoints.baseUrl; +static const Map _headers = { +'Content-Type': 'application/json', +}; - /// Currently selected tenant - static Tenant? currentTenant; +static Tenant? currentTenant; - /// Set the selected tenant - static void setSelectedTenant(Tenant tenant) { - currentTenant = tenant; - } - - /// Check if tenant is selected - static bool get isTenantSelected => currentTenant != null; - - /// Build authorized headers - static Future> _authorizedHeaders() async { - final token = await LocalStorage.getJwtToken(); - if (token == null || token.isEmpty) { - throw Exception('Missing JWT token'); - } - return {..._headers, 'Authorization': 'Bearer $token'}; - } - - /// Handle API errors - static void _handleApiError( - http.Response response, dynamic data, String context) { - final message = data['message'] ?? 'Unknown error'; - final level = - response.statusCode >= 500 ? LogLevel.error : LogLevel.warning; - logSafe("❌ $context failed: $message [Status: ${response.statusCode}]", - level: level); - } - - /// Log exceptions - static void _logException(dynamic e, dynamic st, String context) { - logSafe("❌ $context exception", - level: LogLevel.error, error: e, stackTrace: st); - } - - @override - Future>?> getTenants( - {bool hasRetried = false}) async { - try { - final headers = await _authorizedHeaders(); - - final response = await http.get( - Uri.parse("$_baseUrl/auth/get/user/tenants"), - headers: headers, - ); - - // ✅ Handle empty response BEFORE decoding - if (response.body.isEmpty || response.body.trim().isEmpty) { - logSafe("❌ Empty tenant response — auto logout"); - await LocalStorage.logout(); - return null; - } - - Map data; - try { - data = jsonDecode(response.body); - } catch (e) { - logSafe("❌ Invalid JSON in tenant response — auto logout"); - await LocalStorage.logout(); - return null; - } - - // SUCCESS CASE - if (response.statusCode == 200 && data['success'] == true) { - final list = data['data']; - if (list is! List) return null; - return List>.from(list); - } - - // TOKEN EXPIRED - if (response.statusCode == 401 && !hasRetried) { - final refreshed = await AuthService.refreshToken(); - if (refreshed) return getTenants(hasRetried: true); - return null; - } - - _handleApiError(response, data, "Fetching tenants"); - return null; - } catch (e, st) { - _logException(e, st, "Get Tenants API"); - return null; - } - } - - @override - Future selectTenant(String tenantId, {bool hasRetried = false}) async { - try { - final headers = await _authorizedHeaders(); - logSafe( - "➡️ POST $_baseUrl/auth/select-tenant/$tenantId\nHeaders: $headers", - level: LogLevel.info); - - final response = await http.post( - Uri.parse("$_baseUrl/auth/select-tenant/$tenantId"), - headers: headers, - ); - final data = jsonDecode(response.body); - - logSafe( - "⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]", - level: LogLevel.info); - - if (response.statusCode == 200 && data['success'] == true) { - await LocalStorage.setJwtToken(data['data']['token']); - await LocalStorage.setRefreshToken(data['data']['refreshToken']); - logSafe("✅ Tenant selected successfully. Tokens updated."); - - // 🔥 Refresh projects when tenant changes - try { - final projectController = Get.find(); - projectController.clearProjects(); - projectController.fetchProjects(); - } catch (_) { - logSafe("⚠️ ProjectController not found while refreshing projects"); - } - - // 🔹 Register FCM token after tenant selection - final fcmToken = LocalStorage.getFcmToken(); - if (fcmToken?.isNotEmpty ?? false) { - final success = await AuthService.registerDeviceToken(fcmToken!); - logSafe( - success - ? "✅ FCM token registered after tenant selection." - : "⚠️ Failed to register FCM token after tenant selection.", - level: success ? LogLevel.info : LogLevel.warning); - } - - return true; - } - - if (response.statusCode == 401 && !hasRetried) { - logSafe("⚠️ Unauthorized while selecting tenant. Refreshing token...", - level: LogLevel.warning); - final refreshed = await AuthService.refreshToken(); - if (refreshed) return selectTenant(tenantId, hasRetried: true); - logSafe("❌ Token refresh failed while selecting tenant.", - level: LogLevel.error); - return false; - } - - _handleApiError(response, data, "Selecting tenant"); - return false; - } catch (e, st) { - _logException(e, st, "Select Tenant API"); - return false; - } - } +static void setSelectedTenant(Tenant tenant) { +currentTenant = tenant; } + +static bool get isTenantSelected => currentTenant != null; + +static Future> _authorizedHeaders() async { +final token = await LocalStorage.getJwtToken(); +if (token == null || token.isEmpty) throw Exception('Missing JWT token'); +return {..._headers, 'Authorization': 'Bearer $token'}; +} + +static void _handleApiError(http.Response response, dynamic data, String context) { +final message = data['message'] ?? 'Unknown error'; +final level = response.statusCode >= 500 ? LogLevel.error : LogLevel.warning; +logSafe("❌ $context failed: $message [Status: ${response.statusCode}]", level: level); +} + +static void _logException(dynamic e, dynamic st, String context) { +logSafe("❌ $context exception", level: LogLevel.error, error: e, stackTrace: st); +} + +@override +Future>?> getTenants({bool hasRetried = false}) async { +try { +final headers = await _authorizedHeaders(); +final response = await http.get(Uri.parse("$_baseUrl/auth/get/user/tenants"), headers: headers); + + if (response.body.isEmpty || response.body.trim().isEmpty) { + logSafe("❌ Empty tenant response — auto logout"); + await LocalStorage.logout(); + return null; + } + + final decrypted = decryptResponse(response.body); + if (decrypted == null) { + logSafe("❌ Tenant response decryption failed — auto logout"); + await LocalStorage.logout(); + return null; + } + + final data = decrypted is Map ? decrypted : null; + if (data == null) { + logSafe("❌ Decrypted tenant data is not valid JSON — auto logout"); + await LocalStorage.logout(); + return null; + } + + if (response.statusCode == 200 && data['success'] == true) { + final list = data['data']; + if (list is! List) return null; + return List>.from(list); + } + + if (response.statusCode == 401 && !hasRetried) { + final refreshed = await AuthService.refreshToken(); + if (refreshed) return getTenants(hasRetried: true); + return null; + } + + _handleApiError(response, data, "Fetching tenants"); + return null; +} catch (e, st) { + _logException(e, st, "Get Tenants API"); + return null; +} + + +} + +@override +Future selectTenant(String tenantId, {bool hasRetried = false}) async { +try { +final headers = await _authorizedHeaders(); +logSafe("➡️ POST $_baseUrl/auth/select-tenant/$tenantId\nHeaders: $headers", level: LogLevel.info); + + final response = await http.post(Uri.parse("$_baseUrl/auth/select-tenant/$tenantId"), headers: headers); + final decrypted = decryptResponse(response.body); + if (decrypted == null) { + logSafe("❌ Tenant selection response decryption failed", level: LogLevel.error); + return false; + } + + final data = decrypted is Map ? decrypted : null; + if (data == null) { + logSafe("❌ Decrypted tenant selection data is not valid JSON", level: LogLevel.error); + return false; + } + + logSafe("⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]", level: LogLevel.info); + + if (response.statusCode == 200 && data['success'] == true) { + await LocalStorage.setJwtToken(data['data']['token']); + await LocalStorage.setRefreshToken(data['data']['refreshToken']); + logSafe("✅ Tenant selected successfully. Tokens updated."); + + try { + final projectController = Get.find(); + projectController.clearProjects(); + projectController.fetchProjects(); + } catch (_) { + logSafe("⚠️ ProjectController not found while refreshing projects"); + } + + final fcmToken = LocalStorage.getFcmToken(); + if (fcmToken?.isNotEmpty ?? false) { + final success = await AuthService.registerDeviceToken(fcmToken!); + logSafe(success ? "✅ FCM token registered after tenant selection." : "⚠️ Failed to register FCM token.", level: success ? LogLevel.info : LogLevel.warning); + } + + return true; + } + + if (response.statusCode == 401 && !hasRetried) { + logSafe("⚠️ Unauthorized while selecting tenant. Refreshing token...", level: LogLevel.warning); + final refreshed = await AuthService.refreshToken(); + if (refreshed) return selectTenant(tenantId, hasRetried: true); + logSafe("❌ Token refresh failed while selecting tenant.", level: LogLevel.error); + return false; + } + + _handleApiError(response, data, "Selecting tenant"); + return false; +} catch (e, st) { + _logException(e, st, "Select Tenant API"); + return false; +} + + +} +} \ No newline at end of file diff --git a/lib/helpers/utils/encryption_helper.dart b/lib/helpers/utils/encryption_helper.dart new file mode 100644 index 0000000..82e0b6e --- /dev/null +++ b/lib/helpers/utils/encryption_helper.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; +import 'package:encrypt/encrypt.dart'; +import 'package:on_field_work/helpers/services/app_logger.dart'; // <-- for logging + +// 🔑 CONSTANTS +// Base64-encoded 32-byte key (256 bits for AES-256) +const String _keyBase64 = "u4J7p9Qx2hF5vYtLz8Kq3mN1sG0bRwXyZcD6eH8jFQw="; +// IV must be 16 bytes for AES-CBC mode +const int _ivLength = 16; + +/// Decrypts a Base64-encoded string that contains the IV prepended to the ciphertext. +/// Returns the decoded JSON object, the plain decrypted string, or null on failure. +dynamic decryptResponse(String encryptedBase64Str) { + try { + // 1️⃣ Initialize Key + final rawKeyBytes = base64.decode(_keyBase64); + if (rawKeyBytes.length != 32) { + logSafe("ERROR: Decoded key length is ${rawKeyBytes.length}. Expected 32 bytes for AES-256.", level: LogLevel.error); + throw Exception("Invalid key length."); + } + final key = Key(rawKeyBytes); + + // 2️⃣ Decode incoming encrypted payload (IV + Ciphertext) + final fullBytes = base64.decode(encryptedBase64Str); + + if (fullBytes.length < _ivLength + 16) { + // Minimum length check (16 bytes IV + 1 block of ciphertext, which is 16 bytes) + throw Exception("Encrypted string too short or corrupted."); + } + + // 3️⃣ Extract IV & Ciphertext + // Assumes the first 16 bytes are the IV + final iv = IV(fullBytes.sublist(0, _ivLength)); + final cipherTextBytes = fullBytes.sublist(_ivLength); + + // 4️⃣ Configure Encrypter with specific parameters + // AES-256 with CBC mode and standard PKCS7 padding + final encrypter = Encrypter( + AES( + key, + mode: AESMode.cbc, + padding: 'PKCS7' + ) + ); + final encrypted = Encrypted(cipherTextBytes); + + // 5️⃣ Decrypt - This is where the "Invalid or corrupted pad block" error occurs + final decryptedBytes = encrypter.decryptBytes(encrypted, iv: iv); + final decryptedString = utf8.decode(decryptedBytes); + + if (decryptedString.isEmpty) { + throw Exception("Decryption produced empty string (check if padding was correct)."); + } + + // 🔹 Log decrypted snippet for verification + final snippetLength = decryptedString.length > 50 ? 50 : decryptedString.length; + logSafe( + "Decryption successful. Snippet: ${decryptedString.substring(0, snippetLength)}...", + level: LogLevel.info, + ); + + // 6️⃣ Try parsing JSON + try { + return jsonDecode(decryptedString); + } catch (_) { + // return plain string if it's not JSON + logSafe("Decrypted data is not JSON. Returning plain string.", level: LogLevel.warning); + return decryptedString; + } + } catch (e, st) { + // Catch the specific decryption error (e.g., 'Invalid or corrupted pad block') + logSafe("FATAL Decryption failed: $e", level: LogLevel.error, stackTrace: st); + return null; + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 08696ad..637a4ef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,7 +86,8 @@ dependencies: gallery_saver_plus: ^3.2.9 share_plus: ^12.0.1 timeline_tile: ^2.0.0 - + encrypt: ^5.0.3 + dev_dependencies: flutter_test: sdk: flutter