From 28fbc2ad29c54d296d3bb3c704b16653d73acde9 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 8 Dec 2025 10:49:53 +0530 Subject: [PATCH] optimized the api service --- lib/helpers/services/api_service.dart | 5432 +++++++++---------------- 1 file changed, 2019 insertions(+), 3413 deletions(-) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index dbf41d2..0510ca0 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -4,14 +4,18 @@ import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; +// Helper Services import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/api_endpoints.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; +import 'package:on_field_work/helpers/services/app_logger.dart'; +import 'package:on_field_work/helpers/utils/encryption_helper.dart'; + +// Models import 'package:on_field_work/model/dashboard/project_progress_model.dart'; import 'package:on_field_work/model/dashboard/dashboard_tasks_model.dart'; import 'package:on_field_work/model/dashboard/dashboard_teams_model.dart'; -import 'package:on_field_work/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'; @@ -47,52 +51,47 @@ 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 void _log(String message, {LogLevel level = LogLevel.info}) { + if (enableLogs) { + logSafe(message, level: level); + } + } + + /// Utility to get the token, handle expiry, and refresh if needed. static Future _getToken() async { final token = LocalStorage.getJwtToken(); if (token == null) { - logSafe("No JWT token found. Logging out..."); + _log("No JWT token found. Logging out...", level: LogLevel.error); 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); + final difference = expirationDate.difference(DateTime.now()); - if (difference.inMinutes < 2) { - logSafe( - "Access token is about to expire in ${difference.inSeconds}s. Refreshing..."); + if (JwtDecoder.isExpired(token) || difference.inMinutes < 2) { + _log( + "Token expired or near expiry (${difference.inSeconds}s). Attempting refresh...", + level: LogLevel.warning, + ); final refreshed = await AuthService.refreshToken(); if (refreshed) { return LocalStorage.getJwtToken(); } else { - logSafe("Token refresh failed (near expiry). Logging out..."); + _log("Token refresh failed. Logging out.", level: LogLevel.error); await LocalStorage.logout(); return null; } } } catch (e) { - logSafe("Token decoding error: $e", level: LogLevel.error); + _log("Token decoding error: $e", level: LogLevel.error); await LocalStorage.logout(); return null; } @@ -105,364 +104,350 @@ class ApiService { 'Authorization': 'Bearer $token', }; - static void _log(String message, {LogLevel level = LogLevel.info}) { - if (enableLogs) { - logSafe(message, level: level); - } - } + // --- Centralized Response Parsing and Decryption --- - 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 ⚠️ --- + /// Handles decryption, status code checks, and returns the 'data' payload or the full JSON. + static dynamic _parseAndDecryptResponse(http.Response response, + {String label = '', bool returnFullResponse = false}) { final body = response.body.trim(); + _log("$label Encrypted Response (Status ${response.statusCode}): $body", + level: LogLevel.debug); + if (body.isEmpty) { _log("Empty response body for [$label]"); return null; } - final json = decryptResponse(body); // Decrypt and auto-decode JSON + // Decrypt the Base64 string and auto-decode JSON + final decryptedJson = decryptResponse(body); - if (json == null) { - _log("Decryption failed for [$label]. Cannot parse response."); + if (decryptedJson == null) { + _log("Decryption failed for [$label]. Cannot parse response.", + level: LogLevel.error); 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 non-200 or failure scenarios + if (response.statusCode != 200 || + decryptedJson is! Map || + decryptedJson['success'] != true) { + final message = decryptedJson is Map + ? decryptedJson['message'] ?? 'Unknown error' + : 'Invalid decrypted response format.'; + _log("API Error [$label] (Status ${response.statusCode}): $message", + level: LogLevel.warning); + return null; } - // 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"); - } + _log( + "$label Decrypted Data: ${decryptedJson['data'] ?? 'Success (No data field)'}"); - // --- ⚠️ END of Decryption Change ⚠️ --- - return null; + return returnFullResponse ? decryptedJson : decryptedJson['data']; } - static Future _getRequest( + // --- Generic Request Execution Layer --- + + /// Executes any HTTP request (GET, POST, PUT, DELETE, PATCH) safely with token management and retry logic. + static Future _safeApiCall( String endpoint, { + String method = 'GET', Map? queryParams, + dynamic body, + Map? additionalHeaders, + Duration customTimeout = extendedTimeout, bool hasRetried = false, }) async { String? token = await _getToken(); if (token == null) { - logSafe("Token is null. Forcing logout from GET request.", + _log("Token is null. Cannot proceed with $method request.", level: LogLevel.error); - await LocalStorage.logout(); return null; } - final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") - .replace(queryParameters: queryParams); + 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); + final headers = { + ..._headers(token), + if (additionalHeaders != null) ...additionalHeaders, + }; + + _log("Initiating $method request to $uri", level: LogLevel.debug); + if (body != null) { + _log("Request Body: ${body is String ? body : jsonEncode(body)}", + level: LogLevel.debug); + } try { - final response = await http - .get(uri, headers: _headers(token)) - .timeout(extendedTimeout); + http.Response response; + final encodedBody = (body != null) ? jsonEncode(body) : null; - logSafe("Response Status: ${response.statusCode}", level: LogLevel.debug); - logSafe("Response Body: ${response.body}", level: LogLevel.debug); + switch (method) { + case 'GET': + response = + await http.get(uri, headers: headers).timeout(customTimeout); + break; + case 'POST': + response = await http + .post(uri, headers: headers, body: encodedBody) + .timeout(customTimeout); + break; + case 'PUT': + response = await http + .put(uri, headers: headers, body: encodedBody) + .timeout(customTimeout); + break; + case 'DELETE': + response = + await http.delete(uri, headers: headers).timeout(customTimeout); + break; + case 'PATCH': + response = await http + .patch(uri, headers: headers, body: encodedBody) + .timeout(customTimeout); + break; + default: + throw UnsupportedError("Unsupported HTTP method: $method"); + } + + _log("$method Response Status: ${response.statusCode}", + level: LogLevel.debug); if (response.statusCode == 401 && !hasRetried) { - logSafe("Unauthorized (401). Attempting token refresh...", + _log("Unauthorized (401). Attempting token refresh and retry...", level: LogLevel.warning); - if (await AuthService.refreshToken()) { - logSafe("Token refresh succeeded. Retrying request...", - level: LogLevel.info); - return await _getRequest( + return await _safeApiCall( endpoint, + method: method, queryParams: queryParams, - hasRetried: true, + body: body, + additionalHeaders: additionalHeaders, + customTimeout: customTimeout, + hasRetried: true, // Prevent infinite loop ); } - - logSafe("Token refresh failed. Logging out user.", + _log("Token refresh failed on 401. Logging out user.", level: LogLevel.error); await LocalStorage.logout(); } return response; - } catch (e) { - logSafe("HTTP GET Exception: $e", level: LogLevel.error); + } catch (e, stack) { + _log("HTTP $method Exception: $e\n$stack", 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; - } - } + // --- Public API Methods (Utilizing Generic Call) --- /// ============================================ - /// GET PURCHASE INVOICE OVERVIEW (Dashboard) + /// 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; + final queryParams = {}; + if (projectId != null && projectId.isNotEmpty) { + queryParams['projectId'] = projectId; } + + final response = await _safeApiCall( + ApiEndpoints.getPurchaseInvoiceOverview, + method: 'GET', + queryParams: queryParams, + ); + + if (response == null) return null; + + final parsedJson = _parseAndDecryptResponse( + response, + label: "PurchaseInvoiceOverview", + returnFullResponse: true, + ); + if (parsedJson == null) return null; + + return PurchaseInvoiceOverviewResponse.fromJson(parsedJson); } - /// ============================================ - /// 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; + final queryParams = {}; + if (projectId != null && projectId.isNotEmpty) { + queryParams['projectId'] = projectId; } + + final response = await _safeApiCall( + ApiEndpoints.getCollectionOverview, + method: 'GET', + queryParams: queryParams, + ); + + if (response == null) return null; + + final parsedJson = _parseAndDecryptResponse(response, + label: "CollectionOverview", returnFullResponse: true); + + if (parsedJson == null) return null; + + return CollectionOverviewResponse.fromJson(parsedJson); } -// Infra Project Module APIs + static Future getDashboardTasks( + {required String projectId}) async { + final response = await _safeApiCall( + ApiEndpoints.getDashboardTasks, + method: 'GET', + queryParams: {'projectId': projectId}, + ); - /// ================================ - /// GET INFRA PROJECT DETAILS - /// ================================ - static Future getInfraProjectDetails({ + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "DashboardTasks", returnFullResponse: true); + if (jsonResponse == null) return null; + + return DashboardTasks.fromJson(jsonResponse); + } + + static Future getDashboardTeams( + {required String projectId}) async { + final response = await _safeApiCall( + ApiEndpoints.getDashboardTeams, + method: 'GET', + queryParams: {'projectId': projectId}, + ); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "DashboardTeams", returnFullResponse: true); + if (jsonResponse == null) return null; + + return DashboardTeams.fromJson(jsonResponse); + } + + static Future?> getDashboardAttendanceOverview( + String projectId, int days) async { + if (projectId.isEmpty) throw ArgumentError('projectId must not be empty'); + if (days <= 0) throw ArgumentError('days must be greater than 0'); + + final endpoint = + "${ApiEndpoints.getDashboardAttendanceOverview}/$projectId"; + final response = await _safeApiCall( + endpoint, + method: 'GET', + queryParams: {'days': days.toString()}, + ); + + if (response == null) return null; + return _parseAndDecryptResponse(response, + label: 'Dashboard Attendance Overview'); + } + + static Future getProjectProgress({ + required String projectId, + required int days, + DateTime? fromDate, + }) async { + final actualFromDate = fromDate ?? DateTime.now(); + + final queryParams = { + "projectId": projectId, + "days": days.toString(), + "FromDate": + DateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS").format(actualFromDate), + }; + + final response = await _safeApiCall( + ApiEndpoints.getDashboardProjectProgress, + method: 'GET', + queryParams: queryParams, + ); + + if (response == null) return null; + + final parsed = _parseAndDecryptResponse(response, + label: "ProjectProgress", returnFullResponse: true); + if (parsed == null) return null; + + return ProjectResponse.fromJson(parsed); + } + + static Future getPendingExpensesApi({ required String projectId, }) async { - final endpoint = "${ApiEndpoints.getInfraProjectDetail}/$projectId"; + final response = await _safeApiCall( + ApiEndpoints.getPendingExpenses, + method: 'GET', + queryParams: {'projectId': projectId}, + ); - try { - final response = await _getRequest(endpoint); + if (response == null) return null; - if (response == null) { - _log("getInfraProjectDetails: No response from server", - level: LogLevel.error); - return null; - } + final jsonResponse = _parseAndDecryptResponse(response, + label: "Pending Expenses", returnFullResponse: true); + if (jsonResponse == null) 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; - } + return PendingExpensesResponse.fromJson(jsonResponse); } - /// ================================ - /// GET INFRA PROJECTS LIST - /// ================================ + static Future getExpenseTypeReportApi({ + required String projectId, + required DateTime startDate, + required DateTime endDate, + }) async { + final response = await _safeApiCall( + ApiEndpoints.getExpenseTypeReport, + method: 'GET', + queryParams: { + 'projectId': projectId, + 'startDate': startDate.toIso8601String(), + 'endDate': endDate.toIso8601String(), + }, + ); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Expense Category Report", returnFullResponse: true); + if (jsonResponse == null) return null; + + return ExpenseTypeReportResponse.fromJson(jsonResponse); + } + + static Future + getDashboardMonthlyExpensesApi({ + String? categoryId, + int months = 12, + }) async { + final queryParams = { + 'months': months.toString(), + if (categoryId != null && categoryId.isNotEmpty) 'categoryId': categoryId, + }; + + final response = await _safeApiCall( + ApiEndpoints.getDashboardMonthlyExpenses, + method: 'GET', + queryParams: queryParams, + ); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Dashboard Monthly Expenses", returnFullResponse: true); + if (jsonResponse == null) return null; + + return DashboardMonthlyExpenseResponse.fromJson(jsonResponse); + } + + /// ============================================ + /// INFRA PROJECT + /// ============================================ + static Future getInfraProjectsList({ int pageSize = 20, int pageNumber = 1, @@ -474,153 +459,290 @@ class ApiService { "searchString": searchString, }; - try { - final response = await _getRequest( - ApiEndpoints.getInfraProjectsList, - queryParams: queryParams, - ); + final response = await _safeApiCall( + ApiEndpoints.getInfraProjectsList, + method: 'GET', + queryParams: queryParams, + ); - if (response == null) { - _log("getInfraProjectsList: No response from server", - level: LogLevel.error); - return null; - } + if (response == null) return null; - final parsedJson = - _parseResponseForAllData(response, label: "InfraProjectsList"); + final parsedJson = _parseAndDecryptResponse(response, + label: "InfraProjectsList", returnFullResponse: true); + if (parsedJson == null) return null; - if (parsedJson == null) return null; - - return ProjectsResponse.fromJson(parsedJson); - } catch (e, stack) { - _log("Exception in getInfraProjectsList: $e\n$stack", - level: LogLevel.error); - return null; - } + return ProjectsResponse.fromJson(parsedJson); } - static Future getJobCommentList({ - required String jobTicketId, + static Future getInfraProjectDetails({ + required String projectId, + }) async { + final endpoint = "${ApiEndpoints.getInfraProjectDetail}/$projectId"; + final response = await _safeApiCall(endpoint, method: 'GET'); + + if (response == null) return null; + + final parsedJson = _parseAndDecryptResponse(response, + label: "InfraProjectDetails", returnFullResponse: true); + if (parsedJson == null) return null; + + return ProjectDetailsResponse.fromJson(parsedJson); + } + + /// ============================================ + /// SERVICE PROJECT + /// ============================================ + + static Future getServiceProjectsListApi({ int pageNumber = 1, int pageSize = 20, }) async { final queryParams = { - 'jobTicketId': jobTicketId, 'pageNumber': pageNumber.toString(), 'pageSize': pageSize.toString(), }; - try { - final response = await _getRequest( - ApiEndpoints.getJobCommentList, - queryParams: queryParams, - ); + final response = await _safeApiCall( + ApiEndpoints.getServiceProjectsList, + method: 'GET', + queryParams: queryParams, + ); - if (response == null) { - _log("getJobCommentList: No response from server", - level: LogLevel.error); - return null; - } + if (response == null) return null; - final parsedJson = - _parseResponseForAllData(response, label: "JobCommentList"); - if (parsedJson == null) return null; + final jsonResponse = _parseAndDecryptResponse(response, + label: "Service Project List", returnFullResponse: true); + if (jsonResponse == null) return null; - return JobCommentResponse.fromJson(parsedJson); - } catch (e, stack) { - _log("Exception in getJobCommentList: $e\n$stack", level: LogLevel.error); - return null; - } + return ServiceProjectListModel.fromJson(jsonResponse); } - static Future addJobComment({ - required String jobTicketId, - required String comment, - List> attachments = const [], - }) async { - final body = { - "jobTicketId": jobTicketId, - "comment": comment, - "attachments": attachments, - }; + static Future getServiceProjectDetailApi( + String projectId) async { + final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; + final response = await _safeApiCall(endpoint, method: 'GET'); - try { - final response = await _postRequest( - ApiEndpoints.addJobComment, - body, - ); + if (response == null) return null; - if (response == null) { - _log("addJobComment: No response from server", level: LogLevel.error); - return false; - } + final jsonResponse = _parseAndDecryptResponse(response, + label: "Service Project Detail", returnFullResponse: true); + if (jsonResponse == null) return null; - // 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; - } + return ServiceProjectDetailModel.fromJson(jsonResponse); } - static Future?> getMasterJobStatus({ - required String statusId, + static Future getServiceProjectJobListApi({ required String projectId, + int pageNumber = 1, + int pageSize = 20, + bool isActive = true, + bool isArchive = false, }) async { final queryParams = { - 'statusId': statusId, 'projectId': projectId, + 'pageNumber': pageNumber.toString(), + 'pageSize': pageSize.toString(), + 'isActive': isActive.toString(), + if (isArchive) 'isArchive': 'true', }; - try { - final response = await _getRequest( - ApiEndpoints.getMasterJobStatus, - queryParams: queryParams, - ); + final response = await _safeApiCall( + ApiEndpoints.getServiceProjectJobList, + method: 'GET', + queryParams: queryParams, + ); - if (response == null) { - _log("getMasterJobStatus: No response received."); - return null; - } + if (response == null) return null; - final parsedJson = - _parseResponseForAllData(response, label: "MasterJobStatus"); + final jsonResponse = _parseAndDecryptResponse(response, + label: isArchive + ? "Archived Service Project Job List" + : "Active Service Project Job List", + returnFullResponse: true); - if (parsedJson == null) return null; + if (jsonResponse == 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; - } + return JobResponse.fromJson(jsonResponse); + } + + static Future createServiceProjectJobApi({ + required String title, + required String description, + required String projectId, + required List> assignees, + required DateTime startDate, + required DateTime dueDate, + required List> tags, + required String? branchId, + }) async { + final body = { + "title": title, + "description": description, + "projectId": projectId, + "assignees": assignees, + "startDate": startDate.toIso8601String(), + "dueDate": dueDate.toIso8601String(), + "tags": tags, + "projectBranchId": branchId, + }; + + final response = await _safeApiCall( + ApiEndpoints.createServiceProjectJob, + method: 'POST', + body: body, + ); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse( + response, + label: "Create Service Project Job", + returnFullResponse: true, + ); + + if (jsonResponse == null) return null; + + return jsonResponse['data']?['id']; + } + + static Future getServiceProjectJobDetailApi( + String jobId) async { + final endpoint = "${ApiEndpoints.getServiceProjectJobDetail}/$jobId"; + + final response = await _safeApiCall(endpoint, method: 'GET'); + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Service Project Job Detail", returnFullResponse: true); + if (jsonResponse == null) return null; + + return JobDetailsResponse.fromJson(jsonResponse); + } + + static Future editServiceProjectJobApi({ + required String jobId, + required List> operations, + }) async { + final endpoint = "${ApiEndpoints.editServiceProjectJob}/$jobId"; + // Using PATCH for partial update (JSON Patch format expected by server) + final response = await _safeApiCall( + endpoint, + method: 'PATCH', + body: operations, + ); + + if (response == null) return false; + + // A PATCH request usually returns 200/204. Use parse to check for 'success' flag. + final parsed = _parseAndDecryptResponse(response, + label: "Edit Service Project Job", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + +// Add this method back to ApiService: + static Future?> getEmployees({ + Map? queryParams, + }) async { + final response = await _safeApiCall( + ApiEndpoints.getEmployeesWithoutPermission, + method: 'GET', + queryParams: queryParams, + ); + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Get Employees'); + } + + static Future updateServiceProjectJobAttendance({ + required Map payload, + }) async { + final response = await _safeApiCall( + ApiEndpoints.serviceProjectUpateJobAttendance, + method: 'POST', + body: payload, + ); + + if (response == null) return false; + + // Use parse to check for 'success' flag. + final parsed = _parseAndDecryptResponse(response, + label: "Update Job Attendance", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future getJobAttendanceLog({ + required String attendanceId, + }) async { + final endpoint = + "${ApiEndpoints.serviceProjectUpateJobAttendanceLog}/$attendanceId"; + final response = await _safeApiCall(endpoint, method: 'GET'); + + if (response == null) return null; + + final parsedJson = _parseAndDecryptResponse(response, + label: "JobAttendanceLog", returnFullResponse: true); + if (parsedJson == null) return null; + + return JobAttendanceResponse.fromJson(parsedJson); + } + + static Future?> + getServiceProjectAllocationList({ + required String projectId, + bool isActive = true, + }) async { + final queryParams = { + 'projectId': projectId, + 'isActive': isActive.toString(), + }; + + final response = await _safeApiCall( + ApiEndpoints.getServiceProjectUpateJobAllocationList, + method: 'GET', + queryParams: queryParams, + ); + + if (response == null) return null; + + final data = _parseAndDecryptResponse(response, + label: "ServiceProjectAllocationList"); + + if (data is List) { + return data.map((e) => ServiceProjectAllocation.fromJson(e)).toList(); + } + return null; + } + + static Future manageServiceProjectAllocation({ + required List> payload, + }) async { + final response = await _safeApiCall( + ApiEndpoints.manageServiceProjectUpateJobAllocation, + method: 'POST', + body: payload, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Manage Allocation", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future?> getTeamRoles() async { + final response = + await _safeApiCall(ApiEndpoints.getTeamRoles, method: 'GET'); + + if (response == null) return null; + + final data = _parseAndDecryptResponse(response, label: "TeamRoles"); + if (data is List) { + return data + .map((e) => TeamRole.fromJson(e as Map)) + .toList(); + } + return null; } - /// Fetch Service Project Branches with full response static Future getServiceProjectBranchesFull({ required String projectId, int pageNumber = 1, @@ -637,472 +759,326 @@ class ApiService { 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, + final response = await _safeApiCall( + endpoint, + method: 'GET', queryParams: queryParams, ); - if (response != null) { - return _parseResponse(response, label: ' All Employees Basic'); - } + if (response == null) return null; + final parsedJson = _parseAndDecryptResponse( + response, + label: "ServiceProjectBranchesFull", + returnFullResponse: true, + ); + + if (parsedJson == null) return null; + + return ServiceProjectBranchesResponse.fromJson(parsedJson); + } + + static Future?> getMasterJobStatus({ + required String statusId, + required String projectId, + }) async { + final queryParams = { + 'statusId': statusId, + 'projectId': projectId, + }; + + final response = await _safeApiCall( + ApiEndpoints.getMasterJobStatus, + method: 'GET', + queryParams: queryParams, + ); + + if (response == null) return null; + + final data = _parseAndDecryptResponse(response, label: "MasterJobStatus"); + + if (data is List) { + return data.map((e) => JobStatus.fromJson(e)).toList(); + } return null; } - /// 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"); + 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 _getRequest(endpoint); + final response = await _safeApiCall( + ApiEndpoints.addJobComment, + method: 'POST', + body: body, + ); - if (response == null) { - logSafe( - "Service Project Detail request failed: null response", - level: LogLevel.error, - ); - return null; - } + if (response == null) return false; - final jsonResponse = _parseResponseForAllData( - response, - label: "Service Project Detail", - ); + // Check status 201 for success, or rely on decrypted 'success' flag + final parsed = _parseAndDecryptResponse(response, + label: "AddJobComment", returnFullResponse: true); - 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; + return response.statusCode == 201 || + (parsed != null && parsed['success'] == true); } - /// Get Service Project List - static Future getServiceProjectsListApi({ + static Future getJobCommentList({ + required String jobTicketId, int pageNumber = 1, int pageSize = 20, }) async { - const endpoint = ApiEndpoints.getServiceProjectsList; - logSafe("Fetching Service Project List"); + final queryParams = { + 'jobTicketId': jobTicketId, + 'pageNumber': pageNumber.toString(), + 'pageSize': pageSize.toString(), + }; - try { - final queryParams = { - 'pageNumber': pageNumber.toString(), - 'pageSize': pageSize.toString(), - }; + final response = await _safeApiCall( + ApiEndpoints.getJobCommentList, + method: 'GET', + queryParams: queryParams, + ); - final response = await _getRequest(endpoint, queryParams: queryParams); + if (response == null) return null; - if (response == null) { - logSafe("Service Project List request failed: null response", - level: LogLevel.error); - return null; - } + final parsedJson = _parseAndDecryptResponse(response, + label: "JobCommentList", returnFullResponse: true); + if (parsedJson == null) 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; + return JobCommentResponse.fromJson(parsedJson); + } + + /// ============================================ + /// FINANCE & EXPENSE + /// ============================================ + + static Future + getExpensePaymentRequestPayeeApi() async { + final response = await _safeApiCall( + ApiEndpoints.getExpensePaymentRequestPayee, + method: 'GET'); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Expense Payment Request Payee", returnFullResponse: true); + if (jsonResponse == null) return null; + + return PaymentRequestPayeeResponse.fromJson(jsonResponse); + } + + static Future + getMasterExpenseCategoriesApi() async { + final response = await _safeApiCall( + ApiEndpoints.getMasterExpensesCategories, + method: 'GET'); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Master Expense Categories", returnFullResponse: true); + if (jsonResponse == null) return null; + + return ExpenseCategoryResponse.fromJson(jsonResponse); + } + + static Future getMasterCurrenciesApi() async { + final response = + await _safeApiCall(ApiEndpoints.getMasterCurrencies, method: 'GET'); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Master Currencies", returnFullResponse: true); + if (jsonResponse == null) return null; + + return CurrencyListResponse.fromJson(jsonResponse); + } + + static Future createExpensePaymentRequestApi({ + required String title, + required String projectId, + required String expenseCategoryId, + required String currencyId, + required String payee, + required double amount, + DateTime? dueDate, + required String description, + required bool isAdvancePayment, + List> billAttachments = const [], + }) async { + final body = { + "title": title, + "projectId": projectId, + "expenseCategoryId": expenseCategoryId, + "currencyId": currencyId, + "payee": payee, + "amount": amount, + "dueDate": dueDate?.toIso8601String(), + "description": description, + "isAdvancePayment": isAdvancePayment, + "billAttachments": billAttachments, + }; + + final response = await _safeApiCall( + ApiEndpoints.createExpensePaymentRequest, + method: 'POST', + body: body, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Create Payment Request", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future getExpensePaymentRequestListApi({ + bool isActive = true, + int pageSize = 20, + int pageNumber = 1, + Map? filter, + String searchString = '', + }) async { + final queryParams = { + 'isActive': isActive.toString(), + 'pageSize': pageSize.toString(), + 'pageNumber': pageNumber.toString(), + 'filter': jsonEncode(filter ?? + { + "projectIds": [], + "statusIds": [], + "createdByIds": [], + "currencyIds": [], + "expenseCategoryIds": [], + "payees": [], + "startDate": null, + "endDate": null + }), + 'searchString': searchString, + }; + + final response = await _safeApiCall( + ApiEndpoints.getExpensePaymentRequestList, + method: 'GET', + queryParams: queryParams, + ); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Expense Payment Request List", returnFullResponse: true); + if (jsonResponse == null) return null; + + return PaymentRequestResponse.fromJson(jsonResponse); + } + + static Future + getExpensePaymentRequestFilterApi() async { + final response = await _safeApiCall( + ApiEndpoints.getExpensePaymentRequestFilter, + method: 'GET'); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Expense Payment Request Filter", returnFullResponse: true); + if (jsonResponse == null) return null; + + return PaymentRequestFilter.fromJson(jsonResponse); + } + + static Future getExpensePaymentRequestDetailApi( + String paymentRequestId) async { + final endpoint = + "${ApiEndpoints.getExpensePaymentRequestDetails}/$paymentRequestId"; + + final response = await _safeApiCall(endpoint, method: 'GET'); + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Expense Payment Request Detail", returnFullResponse: true); + if (jsonResponse == null) return null; + + return PaymentRequestDetail.fromJson(jsonResponse); + } + + static Future updateExpensePaymentRequestStatusApi({ + required String paymentRequestId, + required String statusId, + required String comment, + String? paidTransactionId, + String? paidById, + DateTime? paidAt, + double? baseAmount, + double? taxAmount, + String? tdsPercentage, + }) async { + final body = { + "paymentRequestId": paymentRequestId, + "statusId": statusId, + "comment": comment, + "paidTransactionId": paidTransactionId, + "paidById": paidById, + "paidAt": paidAt?.toIso8601String(), + "baseAmount": baseAmount, + "taxAmount": taxAmount, + "tdsPercentage": tdsPercentage ?? "0", + }; + + final response = await _safeApiCall( + ApiEndpoints.updateExpensePaymentRequestStatus, + method: 'POST', + body: body, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Update PR Status", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future createExpenseForPRApi({ + required String paymentModeId, + required String location, + required String gstNumber, + required String statusId, + required String paymentRequestId, + required String comment, + List> billAttachments = const [], + }) async { + final body = { + "paymentModeId": paymentModeId, + "location": location, + "gstNumber": gstNumber, + "statusId": statusId, + "comment": comment, + "paymentRequestId": paymentRequestId, + "billAttachments": billAttachments, + }; + + final response = await _safeApiCall( + ApiEndpoints.createExpenseforPR, + method: 'POST', + body: body, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Create Expense for PR", returnFullResponse: true); + return parsed != null && parsed['success'] == true; } - /// Edit Expense Payment Request static Future editExpensePaymentRequestApi({ required String id, required String title, @@ -1117,7 +1093,6 @@ class ApiService { List> billAttachments = const [], }) async { final endpoint = "${ApiEndpoints.getExpensePaymentRequestEdit}/$id"; - final body = { "id": id, "title": title, @@ -1132,1633 +1107,55 @@ class ApiService { "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, + final response = await _safeApiCall( + endpoint, + method: 'PUT', + body: body, ); - } - /// 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'); + if (response == null) return false; - 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; - } + final parsed = _parseAndDecryptResponse(response, + label: "Edit Expense PR", returnFullResponse: true); + return parsed != null && parsed['success'] == true; } static Future> getAdvancePayments( String employeeId) async { - try { - final endpoint = "${ApiEndpoints.getAdvancePayments}/$employeeId"; + final endpoint = "${ApiEndpoints.getAdvancePayments}/$employeeId"; + final response = await _safeApiCall(endpoint, method: 'GET'); - final response = await _getRequest(endpoint); + if (response == null) return []; - 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 []; + final data = _parseAndDecryptResponse(response, label: 'Advance Payments'); + if (data is List) { + return data.map((e) => AdvancePayment.fromJson(e)).toList(); } + 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); + final response = + await _safeApiCall(ApiEndpoints.getMasterPaymentModes, method: 'GET'); + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Master Payment Modes'); } - /// 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); + final response = + await _safeApiCall(ApiEndpoints.getMasterExpenseStatus, method: 'GET'); + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Master Expense Status'); } - /// 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); + final response = await _safeApiCall(ApiEndpoints.getMasterExpenseCategory, + method: 'GET'); + if (response == null) return null; + return _parseAndDecryptResponse(response, + label: 'Master Expense Categorys'); } - /// Create Expense API static Future createExpenseApi({ required String projectId, required String expensesTypeId, @@ -2788,921 +1185,460 @@ class ApiService { "billAttachments": billAttachments, }; - const endpoint = ApiEndpoints.createExpense; - logSafe("Creating expense with payload: $payload"); + final response = await _safeApiCall( + ApiEndpoints.createExpense, + method: 'POST', + body: payload, + ); - try { - final response = - await _postRequest(endpoint, payload, customTimeout: extendedTimeout); + if (response == null) return false; - 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; + final parsed = _parseAndDecryptResponse(response, + label: "Create Expense", returnFullResponse: true); + return parsed != null && parsed['success'] == true; } - // === 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 + static Future?> getExpenseDetailsApi({ + required String expenseId, }) async { - const endpoint = ApiEndpoints.getDashboardProjectProgress; + final endpoint = "${ApiEndpoints.getExpenseDetails}/$expenseId"; + final response = await _safeApiCall(endpoint, method: 'GET'); + if (response == null) return null; - // Use today's date if fromDate is not provided - final actualFromDate = fromDate ?? DateTime.now(); + // NOTE: This API currently does NOT return encrypted response based on original implementation. + // Assuming the decrypted response returns the whole map containing 'data' + final jsonResponse = _parseAndDecryptResponse(response, + label: "Expense Details", returnFullResponse: true); - 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; + return (jsonResponse != null && + jsonResponse['data'] is Map) + ? jsonResponse['data'] + : 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, + static Future updateExpenseStatusApi({ + required String expenseId, + required String statusId, + String? comment, + String? reimburseTransactionId, + String? reimburseDate, + String? reimbursedById, + double? baseAmount, + double? taxAmount, + double? tdsPercent, + double? netPayable, }) async { final payload = { - "id": id, - "name": name, - "description": description, + "expenseId": expenseId, + "statusId": statusId, + if (comment != null) "comment": comment, + if (reimburseTransactionId != null) + "reimburseTransactionId": reimburseTransactionId, + if (reimburseDate != null) "reimburseDate": reimburseDate, + if (reimbursedById != null) "reimburseById": reimbursedById, + if (baseAmount != null) "baseAmount": baseAmount, + if (taxAmount != null) "taxAmount": taxAmount, + if (tdsPercent != null) "tdsPercent": tdsPercent, + if (netPayable != null) "netPayable": netPayable, }; - final endpoint = "${ApiEndpoints.updateBucket}/$id"; + final response = await _safeApiCall( + ApiEndpoints.updateExpenseStatus, + method: 'POST', + body: payload, + ); - logSafe("Updating bucket with payload: $payload"); + if (response == null) return false; - 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; + final parsed = _parseAndDecryptResponse(response, + label: "Update Expense Status", returnFullResponse: true); + return parsed != null && parsed['success'] == true; } - /// Assign employees to a bucket - static Future assignEmployeesToBucket({ - required String bucketId, - required List> employees, + static Future editExpenseApi({ + required String expenseId, + required Map payload, }) async { - final endpoint = "${ApiEndpoints.assignBucket}/$bucketId"; + final endpoint = "${ApiEndpoints.editExpense}/$expenseId"; + final response = await _safeApiCall( + endpoint, + method: 'PUT', + body: payload, + ); - logSafe("Assigning employees to bucket $bucketId: $employees"); + if (response == null) return false; - 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; + final parsed = _parseAndDecryptResponse(response, + label: "Edit Expense", returnFullResponse: true); + return parsed != null && parsed['success'] == true; } - static Future createBucket({ - required String name, - required String description, - }) async { - final payload = { - "name": name, - "description": description, - }; + static Future deleteExpense(String expenseId) async { + final endpoint = "${ApiEndpoints.deleteExpense}/$expenseId"; - final endpoint = ApiEndpoints.createBucket; + final response = await _safeApiCall(endpoint, method: 'DELETE'); - logSafe("Creating bucket with payload: $payload"); + if (response == null) return false; - 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; + // DELETE requests often return 200 with a success body, or 204 (No Content). + // Rely on parse function which checks for 200 and 'success: true' + final parsed = _parseAndDecryptResponse(response, + label: "Delete Expense", returnFullResponse: true); + return response.statusCode == 204 || + (parsed != null && parsed['success'] == true); } - static Future?> getDirectoryNotes({ - int pageSize = 1000, + static Future?> getExpenseListApi({ + String? filter, + int pageSize = 20, int pageNumber = 1, }) async { final queryParams = { 'pageSize': pageSize.toString(), 'pageNumber': pageNumber.toString(), + if (filter?.isNotEmpty ?? false) 'filter': filter!, }; - final response = await _getRequest( - ApiEndpoints.getDirectoryNotes, + final response = await _safeApiCall( + ApiEndpoints.getExpenseList, + method: 'GET', queryParams: queryParams, ); - final data = response != null - ? _parseResponse(response, label: 'Directory Notes') - : null; + if (response == null) return null; - return data is Map ? data : null; + final jsonResponse = _parseAndDecryptResponse(response, + label: "Expense List", returnFullResponse: true); + return jsonResponse is Map ? jsonResponse : null; } - static Future addContactComment(String note, String contactId) async { - final payload = { - "note": note, - "contactId": contactId, - }; + /// ============================================ + /// DOCUMENT MANAGEMENT + /// ============================================ - final endpoint = ApiEndpoints.updateDirectoryNotes; + static Future getDocumentFilters( + String entityTypeId) async { + final endpoint = "${ApiEndpoints.getDocumentFilter}/$entityTypeId"; + final response = await _safeApiCall(endpoint, method: 'GET'); - logSafe("Adding new comment with payload: $payload"); - logSafe("Sending add comment request to $endpoint"); + if (response == null) return null; - try { - final response = await _postRequest(endpoint, payload); + final jsonResponse = _parseAndDecryptResponse(response, + label: "Document Filters", returnFullResponse: true); + if (jsonResponse == null) return null; - 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; + return DocumentFiltersResponse.fromJson(jsonResponse); } - /// 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, + static Future getDocumentListApi({ + required String entityTypeId, + required String entityId, + String filter = "", + String searchString = "", + bool isActive = true, + int pageNumber = 1, + int pageSize = 20, }) 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 { + "${ApiEndpoints.getDocumentList}/$entityTypeId/entity/$entityId"; final queryParams = { - "active": isActive.toString(), + "filter": filter, + "searchString": searchString, + "isActive": isActive.toString(), + "pageNumber": pageNumber.toString(), + "pageSize": pageSize.toString(), }; - return _getRequest(ApiEndpoints.getDirectoryContacts, - queryParams: queryParams) - .then((res) => - res != null ? _parseResponse(res, label: 'Directory Data') : null); + final response = + await _safeApiCall(endpoint, method: 'GET', queryParams: queryParams); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Document List", returnFullResponse: true); + if (jsonResponse == null) return null; + + return DocumentsResponse.fromJson(jsonResponse); } - static Future?> getContactBucketList() async => - _getRequest(ApiEndpoints.getDirectoryBucketList).then((res) => res != null - ? _parseResponseForAllData(res, label: 'Contact Bucket List') - : null); + static Future getMasterDocumentTagsApi() async { + final response = + await _safeApiCall(ApiEndpoints.getMasterDocumentTags, method: 'GET'); - // === Attendance APIs === + if (response == null) return null; - static Future?> getProjects() async => - _getRequest(ApiEndpoints.getProjects).then( - (res) => res != null ? _parseResponse(res, label: 'Projects') : null); + final jsonResponse = _parseAndDecryptResponse(response, + label: "Master Document Tags", returnFullResponse: true); + if (jsonResponse == null) return null; - static Future?> getGlobalProjects() async => - _getRequest(ApiEndpoints.getGlobalProjects).then((res) => - res != null ? _parseResponse(res, label: 'Global Projects') : null); + return TagResponse.fromJson(jsonResponse); + } - static Future?> getTodaysAttendance( - String projectId, { - String? organizationId, + static Future uploadDocumentApi({ + required String name, + String? documentId, + String? description, + required String entityId, + required String documentTypeId, + required String fileName, + required String base64Data, + required String contentType, + required int fileSize, + String? fileDescription, + bool isActive = true, + List> tags = const [], }) async { - final query = { - "projectId": projectId, - if (organizationId != null) "organizationId": organizationId, + final payload = { + "name": name, + "documentId": documentId ?? "", + "description": description ?? "", + "entityId": entityId, + "documentTypeId": documentTypeId, + "attachment": { + "fileName": fileName, + "base64Data": base64Data, + "contentType": contentType, + "fileSize": fileSize, + "description": fileDescription ?? "", + "isActive": isActive, + }, + "tags": tags.isNotEmpty + ? tags + : [ + {"name": "default", "isActive": true} + ], }; - 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, + final response = await _safeApiCall( + ApiEndpoints.uploadDocument, + method: 'POST', + body: payload, ); 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; + final parsed = _parseAndDecryptResponse(response, + label: "Upload Document", returnFullResponse: true); + return parsed != null && parsed['success'] == true; } - static String generateImageName(String employeeId, int count) { - final now = DateTime.now(); - final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now); - final imageNumber = count.toString().padLeft(3, '0'); - return "${employeeId}_${dateStr}_$imageNumber.jpg"; + static Future getMasterDocumentTypesApi() async { + final response = await _safeApiCall( + ApiEndpoints.getMasterDocumentCategories, + method: 'GET'); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Master Document Types", returnFullResponse: true); + if (jsonResponse == null) return null; + + return DocumentTypesResponse.fromJson(jsonResponse); + } + + static Future getDocumentTypesByCategoryApi( + String documentCategoryId) async { + final response = await _safeApiCall( + ApiEndpoints.getDocumentTypesByCategory, + method: 'GET', + queryParams: {"documentCategoryId": documentCategoryId}, + ); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Document Types by Category", returnFullResponse: true); + if (jsonResponse == null) return null; + + return DocumentTypesResponse.fromJson(jsonResponse); + } + + static Future getDocumentDetailsApi( + String documentId) async { + final endpoint = "${ApiEndpoints.getDocumentDetails}/$documentId"; + final response = await _safeApiCall(endpoint, method: 'GET'); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Document Details", returnFullResponse: true); + if (jsonResponse == null) return null; + + return DocumentDetailsResponse.fromJson(jsonResponse); + } + + static Future getDocumentVersionsApi({ + required String parentAttachmentId, + int pageNumber = 1, + int pageSize = 20, + }) async { + final endpoint = "${ApiEndpoints.getDocumentVersions}/$parentAttachmentId"; + final queryParams = { + "pageNumber": pageNumber.toString(), + "pageSize": pageSize.toString(), + }; + + final response = + await _safeApiCall(endpoint, method: 'GET', queryParams: queryParams); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Document Versions", returnFullResponse: true); + if (jsonResponse == null) return null; + + return DocumentVersionsResponse.fromJson(jsonResponse); + } + + static Future editDocumentApi({ + required String id, + required String name, + required String documentId, + String? description, + List> tags = const [], + Map? attachment, + }) async { + final endpoint = "${ApiEndpoints.editDocument}/$id"; + final payload = { + "id": id, + "name": name, + "documentId": documentId, + "description": description ?? "", + "tags": tags.isNotEmpty + ? tags + : [ + {"name": "default", "isActive": true} + ], + "attachment": attachment, + }; + + final response = await _safeApiCall(endpoint, method: 'PUT', body: payload); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Edit Document", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future deleteDocumentApi({ + required String id, + bool isActive = false, + }) async { + final endpoint = "${ApiEndpoints.deleteDocument}/$id"; + final queryParams = {"isActive": isActive.toString()}; + + // Note: This endpoint is handled via DELETE with query params. + final response = await _safeApiCall( + endpoint, + method: 'DELETE', + queryParams: queryParams, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Delete Document", returnFullResponse: true); + return response.statusCode == 200 && + (parsed != null && parsed['success'] == true); + } + + static Future getPresignedUrlApi(String versionId) async { + final endpoint = "${ApiEndpoints.getDocumentVersion}/$versionId"; + final response = await _safeApiCall(endpoint, method: 'GET'); + + if (response == null) return null; + + final jsonResponse = + _parseAndDecryptResponse(response, label: "Pre-Signed URL"); + + return jsonResponse is String ? jsonResponse : null; + } + + static Future verifyDocumentApi({ + required String id, + bool isVerify = true, + }) async { + final endpoint = "${ApiEndpoints.verifyDocument}/$id"; + final queryParams = {"isVerify": isVerify.toString()}; + + // Note: The original code used POST for this endpoint, retaining that. + final response = await _safeApiCall( + endpoint, + method: 'POST', + queryParams: queryParams, + body: {}, // Must send a body for POST + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Verify Document", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + /// ============================================ + /// EMPLOYEE & ORGANIZATION + /// ============================================ + + static Future?> allEmployeesBasic({ + bool allEmployee = true, + }) async { + final response = await _safeApiCall( + ApiEndpoints.getEmployeesWithoutPermission, + method: 'GET', + queryParams: {'allEmployee': allEmployee.toString()}, + ); + + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'All Employees Basic'); } - // === 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( + final response = await _safeApiCall( ApiEndpoints.getEmployeesWithoutPermission, + method: 'GET', queryParams: queryParams, ); - if (response != null) { - return _parseResponse(response, label: 'Search Employees Basic'); - } - - return null; + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Search Employees Basic'); } static Future?> getAllEmployeesByProject(String projectId, {String? organizationId}) async { if (projectId.isEmpty) throw ArgumentError('projectId must not be empty'); - // Build the endpoint with optional organizationId query - var endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId"; - if (organizationId != null && organizationId.isNotEmpty) { - endpoint += "?organizationId=$organizationId"; - } + final endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId"; + final queryParams = { + if (organizationId != null && organizationId.isNotEmpty) + "organizationId": organizationId + }; - return _getRequest(endpoint).then( - (res) => res != null - ? _parseResponse(res, label: 'Employees by Project') - : null, + final response = await _safeApiCall( + endpoint, + method: 'GET', + queryParams: queryParams, ); + if (response == null) return null; + + return _parseAndDecryptResponse(response, label: 'Employees by Project'); } - /// 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'); - } + 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; @@ -3713,32 +1649,40 @@ class ApiService { final endpoint = "${ApiEndpoints.getAllEmployeesByOrganization}/$projectId"; - final response = await _getRequest(endpoint, queryParams: queryParams); + final response = await _safeApiCall( + endpoint, + method: 'GET', + queryParams: queryParams, + ); - if (response != null) { - return _parseResponse(response, label: 'Employees by Project Service'); - } else { - return null; - } + if (response == null) return null; + return _parseAndDecryptResponse(response, + label: 'Employees by Project Service'); } static Future?> getAllEmployees( {String? organizationId}) async { - var endpoint = ApiEndpoints.getAllEmployees; + final queryParams = { + if (organizationId != null && organizationId.isNotEmpty) + "organizationId": organizationId + }; - // 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, + final response = await _safeApiCall( + ApiEndpoints.getAllEmployees, + method: 'GET', + queryParams: queryParams, ); + + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'All Employees'); + } + + static Future?> getRoles() async { + final response = await _safeApiCall(ApiEndpoints.getRoles, method: 'GET'); + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Roles'); } - static Future?> getRoles() async => - _getRequest(ApiEndpoints.getRoles).then( - (res) => res != null ? _parseResponse(res, label: 'Roles') : null); static Future?> createEmployee({ String? id, required String firstName, @@ -3766,17 +1710,20 @@ class ApiService { "hasApplicationAccess": hasApplicationAccess, }; - final response = await _postRequest( + final response = await _safeApiCall( ApiEndpoints.createEmployee, - body, - customTimeout: extendedTimeout, + method: 'POST', + body: body, ); if (response == null) return null; - final json = jsonDecode(response.body); + final json = + jsonDecode(response.body); // Non-encrypted body expected for this path? + + // Assuming we need to check the raw JSON status since the model is unclear return { - "success": response.statusCode == 200 && json['success'] == true, + "success": response.statusCode == 200 && (json['success'] == true), "data": json, }; } @@ -3784,43 +1731,176 @@ class ApiService { 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; + final response = await _safeApiCall(url, method: 'GET'); + if (response == null) return null; + + final data = _parseAndDecryptResponse(response, label: 'Employee Details'); return data is Map ? data : null; } - // === Daily Task APIs === - /// Get Daily Task Project Report Filter + static Future?> getAssignedProjects(String employeeId) async { + if (employeeId.isEmpty) throw ArgumentError("employeeId must not be empty"); + + final endpoint = "${ApiEndpoints.getAssignedProjects}/$employeeId"; + final response = await _safeApiCall(endpoint, method: 'GET'); + + if (response == null) return null; + + final parsed = + _parseAndDecryptResponse(response, label: "Assigned Projects"); + return parsed is List ? parsed : null; + } + + static Future assignProjects({ + required String employeeId, + required List> projects, + }) async { + if (employeeId.isEmpty) throw ArgumentError("employeeId must not be empty"); + if (projects.isEmpty) + _log("Warning: Projects list is empty in assignProjects."); + + final endpoint = "${ApiEndpoints.assignProjects}/$employeeId"; + + final response = await _safeApiCall( + endpoint, + method: 'POST', + body: projects, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Assign Projects", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future?> getOrganizationHierarchyList( + String employeeId) async { + if (employeeId.isEmpty) throw ArgumentError('employeeId must not be empty'); + final endpoint = "${ApiEndpoints.getOrganizationHierarchyList}/$employeeId"; + + final response = await _safeApiCall(endpoint, method: 'GET'); + if (response == null) return null; + + return _parseAndDecryptResponse(response, + label: 'Organization Hierarchy List'); + } + + static Future manageOrganizationHierarchy({ + required String employeeId, + required List> payload, + }) async { + if (employeeId.isEmpty) throw ArgumentError('employeeId must not be empty'); + + final endpoint = "${ApiEndpoints.manageOrganizationHierarchy}/$employeeId"; + + final response = await _safeApiCall( + endpoint, + method: 'POST', + body: payload, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Manage Hierarchy", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + /// ============================================ + /// PROJECT MANAGEMENT + /// ============================================ + + static Future createProjectApi({ + required String name, + required String projectAddress, + required String shortName, + required String contactPerson, + required DateTime startDate, + required DateTime endDate, + required String projectStatusId, + }) async { + final payload = { + "name": name, + "projectAddress": projectAddress, + "shortName": shortName, + "contactPerson": contactPerson, + "startDate": startDate.toIso8601String(), + "endDate": endDate.toIso8601String(), + "projectStatusId": projectStatusId, + }; + + final response = await _safeApiCall( + ApiEndpoints.createProject, + method: 'POST', + body: payload, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Create Project", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future getAssignedOrganizations( + String projectId) async { + final endpoint = "${ApiEndpoints.getAssignedOrganizations}/$projectId"; + final response = await _safeApiCall(endpoint, method: 'GET'); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Assigned Organizations", returnFullResponse: true); + if (jsonResponse == null) return null; + + return OrganizationListResponse.fromJson(jsonResponse); + } + + static Future getAllOrganizations() async { + final response = + await _safeApiCall(ApiEndpoints.getAllOrganizations, method: 'GET'); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "All Organizations", returnFullResponse: true); + if (jsonResponse == null) return null; + + return AllOrganizationListResponse.fromJson(jsonResponse); + } + + static Future getAssignedServices( + String projectId) async { + final endpoint = "${ApiEndpoints.getAssignedServices}/$projectId"; + final response = await _safeApiCall(endpoint, method: 'GET'); + + if (response == null) return null; + + final jsonResponse = _parseAndDecryptResponse(response, + label: "Assigned Services", returnFullResponse: true); + if (jsonResponse == null) return null; + + return ServiceListResponse.fromJson(jsonResponse); + } + + /// ============================================ + /// DAILY TASK PLANNING + /// ============================================ + static Future getDailyTaskFilter( String projectId) async { final endpoint = "${ApiEndpoints.getDailyTaskProjectProgressFilter}/$projectId"; - logSafe("Fetching daily task Progress filter for projectId: $projectId"); + final response = await _safeApiCall(endpoint, method: 'GET'); - try { - final response = await _getRequest(endpoint); + if (response == null) return null; - if (response == null) { - logSafe("Daily task filter request failed: null response", - level: LogLevel.error); - return null; - } + final jsonResponse = _parseAndDecryptResponse(response, + label: "Daily Task Progress Filter", returnFullResponse: true); + if (jsonResponse == null) 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; + return DailyProgressReportFilterResponse.fromJson(jsonResponse); } static Future?> getDailyTasks( @@ -3829,7 +1909,6 @@ class ApiService { int pageNumber = 1, int pageSize = 20, }) async { - // Build query parameters final query = { "projectId": projectId, "pageNumber": pageNumber.toString(), @@ -3837,15 +1916,17 @@ class ApiService { if (filter != null) "filter": jsonEncode(filter), }; - final uri = - Uri.parse(ApiEndpoints.getDailyTask).replace(queryParameters: query); + final response = await _safeApiCall( + ApiEndpoints.getDailyTask, + method: 'GET', + queryParams: query, + ); - final response = await _getRequest(uri.toString()); - final parsed = response != null - ? _parseResponse(response, label: 'Daily Tasks') - : null; + if (response == null) return null; - if (parsed != null && parsed['data'] != null) { + final parsed = _parseAndDecryptResponse(response, label: 'Daily Tasks'); + + if (parsed != null && parsed is Map && parsed['data'] is List) { return (parsed['data'] as List) .map((e) => TaskModel.fromJson(e)) .toList(); @@ -3870,19 +1951,20 @@ class ApiService { if (images != null && images.isNotEmpty) "images": images, }; - final response = await _postRequest( + final response = await _safeApiCall( ApiEndpoints.reportTask, - body, - customTimeout: extendedTimeout, + method: 'POST', + body: body, ); if (response == null) return false; - final json = jsonDecode(response.body); - if (response.statusCode == 200 && json['success'] == true) { - Get.back(); + + final parsed = _parseAndDecryptResponse(response, + label: "Report Task", returnFullResponse: true); + if (parsed != null && parsed['success'] == true) { + Get.back(); // Retaining Get.back() as per original logic return true; } - logSafe("Failed to report task: ${json['message'] ?? 'Unknown error'}"); return false; } @@ -3898,50 +1980,54 @@ class ApiService { if (images != null && images.isNotEmpty) "images": images, }; - final response = await _postRequest(ApiEndpoints.commentTask, body); + final response = await _safeApiCall( + ApiEndpoints.commentTask, + method: 'POST', + body: body, + ); + if (response == null) return false; - final json = jsonDecode(response.body); - return response.statusCode == 200 && json['success'] == true; + + final parsed = _parseAndDecryptResponse(response, + label: "Comment Task", returnFullResponse: true); + return parsed != null && parsed['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"; + final queryParams = { + if (serviceId != null && serviceId.isNotEmpty) 'serviceId': serviceId, + }; - if (serviceId != null && serviceId.isNotEmpty) { - endpoint += "?serviceId=$serviceId"; - } + final response = await _safeApiCall( + endpoint, + method: 'GET', + queryParams: queryParams, + ); + if (response == null) return null; - 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?; + return _parseAndDecryptResponse(response, + label: 'Infra Details', + returnFullResponse: true) 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"; + final queryParams = { + if (serviceId != null && serviceId.isNotEmpty) 'serviceId': serviceId, + }; - if (serviceId != null && serviceId.isNotEmpty) { - endpoint += "?serviceId=$serviceId"; - } + final response = await _safeApiCall( + endpoint, + method: 'GET', + queryParams: queryParams, + ); + if (response == null) return null; - 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?; + return _parseAndDecryptResponse(response, + label: 'Work Items', returnFullResponse: true) as Map?; } static Future assignDailyTask({ @@ -3963,35 +2049,42 @@ class ApiService { "assignmentDate": (assignmentDate ?? DateTime.now()).toUtc().toIso8601String(), }; - final response = await _postRequest(ApiEndpoints.assignDailyTask, body); + final response = await _safeApiCall( + ApiEndpoints.assignDailyTask, + method: 'POST', + body: body, + ); + if (response == null) return false; - final json = jsonDecode(response.body); - if (response.statusCode == 200 && json['success'] == true) { - Get.back(); + + final parsed = _parseAndDecryptResponse(response, + label: "Assign Daily Task", returnFullResponse: true); + if (parsed != null && parsed['success'] == true) { + Get.back(); // Retaining Get.back() as per original logic return true; } - 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; - } + final response = + await _safeApiCall(ApiEndpoints.getWorkStatus, method: 'GET'); + if (response == null) return null; - logSafe('Work Status raw response: ${res.body}'); - return _parseResponseForAllData(res, label: 'Work Status') - as Map?; + return _parseAndDecryptResponse(response, + label: 'Work Status', + returnFullResponse: true) as Map?; } - static Future?> getMasterWorkCategories() async => - _getRequest(ApiEndpoints.getmasterWorkCategories).then((res) => - res != null - ? _parseResponseForAllData(res, label: 'Master Work Categories') - : null); + static Future?> getMasterWorkCategories() async { + final response = + await _safeApiCall(ApiEndpoints.getmasterWorkCategories, method: 'GET'); + if (response == null) return null; + + return _parseAndDecryptResponse(response, + label: 'Master Work Categories', + returnFullResponse: true) as Map?; + } static Future approveTask({ required String id, @@ -4008,11 +2101,17 @@ class ApiService { if (images != null && images.isNotEmpty) "images": images, }; - final response = await _postRequest(ApiEndpoints.approveReportAction, body); + final response = await _safeApiCall( + ApiEndpoints.approveReportAction, + method: 'POST', + body: body, + ); + if (response == null) return false; - final json = jsonDecode(response.body); - return response.statusCode == 200 && json['success'] == true; + final parsed = _parseAndDecryptResponse(response, + label: "Approve Task", returnFullResponse: true); + return parsed != null && parsed['success'] == true; } static Future createTask({ @@ -4036,16 +2135,523 @@ class ApiService { } ]; - final response = await _postRequest(ApiEndpoints.assignTask, body); + final response = await _safeApiCall( + ApiEndpoints.assignTask, + method: 'POST', + body: body, + ); + if (response == null) return false; - final json = jsonDecode(response.body); - if (response.statusCode == 200 && json['success'] == true) { - Get.back(); + final parsed = _parseAndDecryptResponse(response, + label: "Create Task", returnFullResponse: true); + if (parsed != null && parsed['success'] == true) { + Get.back(); // Retaining Get.back() as per original logic return true; } - - logSafe("Failed to create task: ${json['message'] ?? 'Unknown error'}"); return false; } + + /// ============================================ + /// LOGGING & MENU + /// ============================================ + + static Future postLogsApi(List> logs) async { + // NOTE: This call retrieves the token directly and bypasses refresh logic + // to avoid an infinite loop if logging itself fails. + + final token = await LocalStorage.getJwtToken(); + if (token == null) { + _log("No token available. Skipping logs post.", level: LogLevel.warning); + return false; + } + + final uri = Uri.parse("${ApiEndpoints.baseUrl}${ApiEndpoints.uploadLogs}"); + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + try { + final response = await http + .post(uri, headers: headers, body: jsonEncode(logs)) + .timeout(ApiService.extendedTimeout); + + _log("Post logs response status: ${response.statusCode}"); + + // Decryption chain for API response + final decryptedJson = decryptResponse(response.body); + + if (response.statusCode == 200 && + decryptedJson is Map && + decryptedJson['success'] == true) { + _log("Logs posted successfully."); + return true; + } + _log( + "Failed to post logs: ${decryptedJson?['message'] ?? 'Unknown error'}", + level: LogLevel.warning); + } catch (e, stack) { + _log("Exception during postLogsApi: $e\n$stack", level: LogLevel.error); + } + return false; + } + + // ApiService.dart (around line 1400) + static Future?> getMenuApi() async { + final response = + await _safeApiCall(ApiEndpoints.getDynamicMenu, method: 'GET'); + if (response == null) return null; + + // Use the centralized parsing/decryption utility. + // The server is clearly sending an encrypted response based on the log error. + final jsonResponse = _parseAndDecryptResponse( + response, + label: "Dynamic Menu", + returnFullResponse: true, + ); + + // Return the full decrypted JSON map if parsing and 'success: true' check passed + return jsonResponse is Map ? jsonResponse : null; + } + + /// ============================================ + /// ATTENDANCE + /// ============================================ + + static Future?> getProjects() async { + final response = + await _safeApiCall(ApiEndpoints.getProjects, method: 'GET'); + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Projects'); + } + + static Future?> getGlobalProjects() async { + final response = + await _safeApiCall(ApiEndpoints.getGlobalProjects, method: 'GET'); + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Global Projects'); + } + + static Future?> getTodaysAttendance( + String projectId, { + String? organizationId, + }) async { + final query = { + "projectId": projectId, + if (organizationId != null) "organizationId": organizationId, + }; + + final response = await _safeApiCall(ApiEndpoints.getTodaysAttendance, + method: 'GET', queryParams: query); + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Employees'); + } + + static Future?> getAttendanceForDashboard( + String projectId) async { + final endpoint = ApiEndpoints.getAttendanceForDashboard.replaceFirst( + ':projectId', + projectId, + ); + + final response = await _safeApiCall(endpoint, method: 'GET'); + if (response == null) return null; + + final data = + _parseAndDecryptResponse(response, label: 'Dashboard Attendance'); + + if (data is Map) { + return [EmployeeModel.fromJson(data)]; + } else if (data is List) { + return data.map((e) => EmployeeModel.fromJson(e)).toList(); + } + return null; + } + + static Future?> getRegularizationLogs( + String projectId, { + String? organizationId, + }) async { + final query = { + "projectId": projectId, + if (organizationId != null) "organizationId": organizationId, + }; + + final response = await _safeApiCall(ApiEndpoints.getRegularizationLogs, + method: 'GET', queryParams: query); + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Regularization Logs'); + } + + static Future?> getAttendanceLogs( + String projectId, { + DateTime? dateFrom, + DateTime? dateTo, + String? organizationId, + }) async { + final query = { + "projectId": projectId, + if (dateFrom != null) + "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), + if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), + if (organizationId != null) "organizationId": organizationId, + }; + + final response = await _safeApiCall(ApiEndpoints.getAttendanceLogs, + method: 'GET', queryParams: query); + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Attendance Logs'); + } + + static Future?> getAttendanceLogView(String id) async { + final response = await _safeApiCall( + "${ApiEndpoints.getAttendanceLogView}/$id", + method: 'GET'); + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Log Details'); + } + + static Future uploadAttendanceImage( + String id, + String employeeId, + XFile? imageFile, + double latitude, + double longitude, { + required String imageName, + required String projectId, + String comment = "", + required int action, + bool imageCapture = true, + required String markTime, + required String date, + }) async { + final body = { + "id": id, + "employeeId": employeeId, + "projectId": projectId, + "markTime": markTime, + "comment": comment, + "action": action, + "date": date, + if (imageCapture) "latitude": '$latitude', + if (imageCapture) "longitude": '$longitude', + }; + + if (imageCapture && imageFile != null) { + try { + final bytes = await imageFile.readAsBytes(); + final fileSize = await imageFile.length(); + final contentType = "image/${imageFile.path.split('.').last}"; + body["image"] = { + "fileName": imageName, + "contentType": contentType, + "fileSize": fileSize, + "description": "Employee attendance photo", + "base64Data": base64Encode(bytes), + }; + } catch (e) { + _log("Image encoding error: $e", level: LogLevel.error); + return false; + } + } + + final response = await _safeApiCall( + ApiEndpoints.uploadAttendanceImage, + method: 'POST', + body: body, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Upload Attendance Image", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static String generateImageName(String employeeId, int count) { + final now = DateTime.now(); + final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now); + final imageNumber = count.toString().padLeft(3, '0'); + return "${employeeId}_${dateStr}_$imageNumber.jpg"; + } + + /// ============================================ + /// DIRECTORY + /// ============================================ + + static Future?> getContactBucketList() async { + final response = + await _safeApiCall(ApiEndpoints.getDirectoryBucketList, method: 'GET'); + if (response == null) return null; + return _parseAndDecryptResponse(response, + label: 'Contact Bucket List', + returnFullResponse: true) as Map?; + } + + static Future?> getDirectoryData( + {required bool isActive}) async { + final queryParams = {"active": isActive.toString()}; + + final response = await _safeApiCall( + ApiEndpoints.getDirectoryContacts, + method: 'GET', + queryParams: queryParams, + ); + if (response == null) return null; + return _parseAndDecryptResponse(response, label: 'Directory Data'); + } + + static Future?> getContactTagList() async { + final response = + await _safeApiCall(ApiEndpoints.getDirectoryContactTags, method: 'GET'); + if (response == null) return null; + return _parseAndDecryptResponse(response, + label: 'Contact Tag List', + returnFullResponse: true) as Map?; + } + + static Future?> getContactCategoryList() async { + final response = await _safeApiCall( + ApiEndpoints.getDirectoryContactCategory, + method: 'GET'); + if (response == null) return null; + return _parseAndDecryptResponse(response, + label: 'Contact Category List', + returnFullResponse: true) as Map?; + } + + static Future> getOrganizationList() async { + final response = await _safeApiCall(ApiEndpoints.getDirectoryOrganization, + method: 'GET'); + if (response == null) return []; + + final body = _parseAndDecryptResponse(response, label: 'Organization List'); + if (body is List) { + return List.from(body); + } + return []; + } + + static Future createContact(Map payload) async { + final response = await _safeApiCall(ApiEndpoints.createContact, + method: 'POST', body: payload); + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Create Contact", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future updateContact( + String contactId, Map payload) async { + final endpoint = "${ApiEndpoints.updateContact}/$contactId"; + final response = await _safeApiCall(endpoint, method: 'PUT', body: payload); + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Update Contact", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future deleteDirectoryContact(String contactId) async { + // Note: The original implementation uses a DELETE request with an 'active=false' query param for soft delete. + final endpoint = "${ApiEndpoints.updateContact}/$contactId/"; + final queryParams = {'active': 'false'}; + + final response = await _safeApiCall( + endpoint, + method: 'DELETE', + queryParams: queryParams, + ); + + if (response == null) return false; + + // Assuming successful deletion is 200/204 + return response.statusCode == 200 || response.statusCode == 204; + } + + static Future restoreDirectoryContact(String contactId) async { + // Note: The original implementation uses a DELETE request with an 'active=true' query param for restore. + final endpoint = "${ApiEndpoints.updateContact}/$contactId/"; + final queryParams = {'active': 'true'}; + + final response = await _safeApiCall( + endpoint, + method: 'DELETE', + queryParams: queryParams, + ); + + if (response == null) return false; + + // Assuming successful restore is 200/204 + return response.statusCode == 200 || response.statusCode == 204; + } + + static Future?> getDirectoryComments( + String contactId, { + bool active = true, + }) async { + final endpoint = "${ApiEndpoints.getDirectoryNotes}/$contactId"; + final queryParams = {'active': active.toString()}; + final response = + await _safeApiCall(endpoint, method: 'GET', queryParams: queryParams); + if (response == null) return null; + + final data = + _parseAndDecryptResponse(response, label: 'Directory Comments'); + return data is List ? data : null; + } + + static Future restoreContactComment( + String commentId, + bool isActive, + ) async { + final endpoint = "${ApiEndpoints.updateDirectoryNotes}/$commentId"; + final queryParams = {'active': isActive.toString()}; + + // Note: The original implementation used DELETE for status update + final response = await _safeApiCall( + endpoint, + method: 'DELETE', + queryParams: queryParams, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Restore/Delete Comment", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future updateContactComment( + String commentId, String note, String contactId) async { + final payload = { + "id": commentId, + "contactId": contactId, + "note": note, + }; + final endpoint = "${ApiEndpoints.updateDirectoryNotes}/$commentId"; + final headers = {"comment-id": commentId}; // Per original logic + + final response = await _safeApiCall( + endpoint, + method: 'PUT', + body: payload, + additionalHeaders: headers, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Update Comment", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future addContactComment(String note, String contactId) async { + final payload = { + "note": note, + "contactId": contactId, + }; + final response = await _safeApiCall( + ApiEndpoints.updateDirectoryNotes, + method: 'POST', + body: payload, + ); + + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Add Comment", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future?> getDirectoryNotes({ + int pageSize = 1000, + int pageNumber = 1, + }) async { + final queryParams = { + 'pageSize': pageSize.toString(), + 'pageNumber': pageNumber.toString(), + }; + + final response = await _safeApiCall( + ApiEndpoints.getDirectoryNotes, + method: 'GET', + queryParams: queryParams, + ); + if (response == null) return null; + + return _parseAndDecryptResponse(response, label: 'Directory Notes') + as Map?; + } + + static Future createBucket({ + required String name, + required String description, + }) async { + final payload = { + "name": name, + "description": description, + }; + final response = await _safeApiCall( + ApiEndpoints.createBucket, + method: 'POST', + body: payload, + ); + if (response == null) return false; + final parsed = _parseAndDecryptResponse(response, + label: "Create Bucket", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future assignEmployeesToBucket({ + required String bucketId, + required List> employees, + }) async { + final endpoint = "${ApiEndpoints.assignBucket}/$bucketId"; + + final response = await _safeApiCall( + endpoint, + method: 'POST', + body: employees, + ); + if (response == null) return false; + final parsed = _parseAndDecryptResponse(response, + label: "Assign Employees to Bucket", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future updateBucket({ + required String id, + required String name, + required String description, + }) async { + final payload = { + "id": id, + "name": name, + "description": description, + }; + final endpoint = "${ApiEndpoints.updateBucket}/$id"; + + final response = await _safeApiCall( + endpoint, + method: 'PUT', + body: payload, + ); + if (response == null) return false; + final parsed = _parseAndDecryptResponse(response, + label: "Update Bucket", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } + + static Future deleteBucket(String id) async { + final endpoint = "${ApiEndpoints.updateBucket}/$id"; + final response = await _safeApiCall(endpoint, method: 'DELETE'); + if (response == null) return false; + + final parsed = _parseAndDecryptResponse(response, + label: "Delete Bucket", returnFullResponse: true); + return parsed != null && parsed['success'] == true; + } }