import 'dart:convert'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:marco/helpers/services/app_logger.dart'; class ApiService { static const Duration timeout = Duration(seconds: 30); static const bool enableLogs = true; static const Duration extendedTimeout = Duration(seconds: 60); static Future _getToken() async { final token = await LocalStorage.getJwtToken(); if (token == null) { logSafe("No JWT token found."); return null; } try { if (JwtDecoder.isExpired(token)) { logSafe("Access token is expired. Attempting refresh..."); final refreshed = await AuthService.refreshToken(); if (refreshed) { return await LocalStorage.getJwtToken(); } else { logSafe("Token refresh failed. Logging out..."); await LocalStorage.logout(); return null; } } final expirationDate = JwtDecoder.getExpirationDate(token); final now = DateTime.now(); final difference = expirationDate.difference(now); if (difference.inMinutes < 2) { logSafe( "Access token is about to expire in ${difference.inSeconds}s. Refreshing..."); final refreshed = await AuthService.refreshToken(); if (refreshed) { return await LocalStorage.getJwtToken(); } } } catch (e) { logSafe("Token decoding error: $e", level: LogLevel.error); } return token; } static Map _headers(String token) => { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }; static void _log(String message) { if (enableLogs) logSafe(message); } static dynamic _parseResponse(http.Response response, {String label = ''}) { _log("$label Response: ${response.body}"); try { final json = jsonDecode(response.body); if (response.statusCode == 200 && json['success'] == true) { return json['data']; } _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); } catch (e) { _log("Response parsing error [$label]: $e"); } return null; } static dynamic _parseResponseForAllData(http.Response response, {String label = ''}) { _log("$label Response: ${response.body}"); try { final body = response.body.trim(); if (body.isEmpty) throw FormatException("Empty response body"); final json = jsonDecode(body); if (response.statusCode == 200 && json['success'] == true) { return json; } _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); } catch (e) { _log("Response parsing error [$label]: $e"); } return null; } static Future _getRequest( String endpoint, { Map? queryParams, bool hasRetried = false, }) async { String? token = await _getToken(); if (token == null) { logSafe("Token is null. Cannot proceed with GET request.", level: LogLevel.error); return null; } final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") .replace(queryParameters: queryParams); logSafe("Initiating GET request", level: LogLevel.debug); logSafe("URL: $uri", level: LogLevel.debug); logSafe("Query Parameters: ${queryParams ?? {}}", level: LogLevel.debug); logSafe("Headers: ${_headers(token)}", level: LogLevel.debug); try { final response = await http.get(uri, headers: _headers(token)).timeout(timeout); logSafe("Response Status: ${response.statusCode}", level: LogLevel.debug); logSafe("Response Body: ${response.body}", level: LogLevel.debug); if (response.statusCode == 401 && !hasRetried) { logSafe("Unauthorized (401). Attempting token refresh...", level: LogLevel.warning); if (await AuthService.refreshToken()) { logSafe("Token refresh succeeded. Retrying request...", level: LogLevel.info); return await _getRequest( endpoint, queryParams: queryParams, hasRetried: true, ); } logSafe("Token refresh failed. Aborting request.", level: LogLevel.error); } return response; } catch (e) { logSafe("HTTP GET Exception: $e", level: LogLevel.error); return null; } } static Future _postRequest( String endpoint, dynamic body, { Duration customTimeout = timeout, 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 = timeout, 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; } } // === Dashboard Endpoints === 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); } /// 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(timeout); logSafe("DELETE bucket response status: ${response.statusCode}"); logSafe("DELETE bucket response body: ${response.body}"); final json = jsonDecode(response.body); if (response.statusCode == 200 && json['success'] == true) { logSafe("Bucket deleted successfully."); return true; } else { logSafe( "Failed to delete bucket: ${json['message'] ?? 'Unknown error'}", level: LogLevel.warning); } } catch (e, stack) { logSafe("Exception during deleteBucket API: $e", level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); } return false; } static Future updateBucket({ required String id, required String name, required String description, }) async { final payload = { "id": id, "name": name, "description": description, }; final endpoint = "${ApiEndpoints.updateBucket}/$id"; logSafe("Updating bucket with payload: $payload"); try { final response = await _putRequest(endpoint, payload); if (response == null) { logSafe("Update bucket failed: null response", level: LogLevel.error); return false; } logSafe("Update bucket response status: ${response.statusCode}"); logSafe("Update bucket response body: ${response.body}"); final json = jsonDecode(response.body); if (json['success'] == true) { logSafe("Bucket updated successfully: ${json['data']}"); return true; } else { logSafe("Failed to update bucket: ${json['message']}", level: LogLevel.warning); } } catch (e, stack) { logSafe("Exception during updateBucket API: $e", level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); } return false; } /// Assign employees to a bucket static Future assignEmployeesToBucket({ required String bucketId, required List> employees, }) async { final endpoint = "${ApiEndpoints.assignBucket}/$bucketId"; logSafe("Assigning employees to bucket $bucketId: $employees"); try { final response = await _postRequest(endpoint, employees); if (response == null) { logSafe("Assign employees failed: null response", level: LogLevel.error); return false; } logSafe("Assign employees response status: ${response.statusCode}"); logSafe("Assign employees response body: ${response.body}"); final json = jsonDecode(response.body); if (json['success'] == true) { logSafe("Employees assigned successfully"); return true; } else { logSafe("Failed to assign employees: ${json['message']}", level: LogLevel.warning); } } catch (e, stack) { logSafe("Exception during assignEmployeesToBucket API: $e", level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); } return false; } static Future createBucket({ required String name, required String description, }) async { final payload = { "name": name, "description": description, }; final endpoint = ApiEndpoints.createBucket; logSafe("Creating bucket with payload: $payload"); try { final response = await _postRequest(endpoint, payload); if (response == null) { logSafe("Create bucket failed: null response", level: LogLevel.error); return false; } logSafe("Create bucket response status: ${response.statusCode}"); logSafe("Create bucket response body: ${response.body}"); final json = jsonDecode(response.body); if (json['success'] == true) { logSafe("Bucket created successfully: ${json['data']}"); return true; } else { logSafe("Failed to create bucket: ${json['message']}", level: LogLevel.warning); } } catch (e, stack) { logSafe("Exception during createBucket API: ${e.toString()}", level: LogLevel.error); logSafe("StackTrace: ${stack.toString()}", level: LogLevel.debug); } return false; } static Future?> getDirectoryNotes({ int pageSize = 1000, int pageNumber = 1, }) async { final queryParams = { 'pageSize': pageSize.toString(), 'pageNumber': pageNumber.toString(), }; final response = await _getRequest( ApiEndpoints.getDirectoryNotes, queryParams: queryParams, ); final data = response != null ? _parseResponse(response, label: 'Directory Notes') : null; return data is Map ? data : null; } static Future addContactComment(String note, String contactId) async { final payload = { "note": note, "contactId": contactId, }; final endpoint = ApiEndpoints.updateDirectoryNotes; logSafe("Adding new comment with payload: $payload"); logSafe("Sending add comment request to $endpoint"); try { final response = await _postRequest(endpoint, payload); if (response == null) { logSafe("Add comment failed: null response", level: LogLevel.error); return false; } logSafe("Add comment response status: ${response.statusCode}"); logSafe("Add comment response body: ${response.body}"); final json = jsonDecode(response.body); if (json['success'] == true) { logSafe("Comment added successfully for contactId: $contactId"); return true; } else { logSafe("Failed to add comment: ${json['message']}", level: LogLevel.warning); } } catch (e, stack) { logSafe("Exception during addComment API: ${e.toString()}", level: LogLevel.error); logSafe("StackTrace: ${stack.toString()}", level: LogLevel.debug); } return false; } 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?> getDirectoryComments(String contactId) async { final url = "${ApiEndpoints.getDirectoryNotes}/$contactId"; final response = await _getRequest(url); final data = response != null ? _parseResponse(response, label: 'Directory Comments') : null; return data is List ? data : null; } static Future updateContact( String contactId, Map payload) async { try { final endpoint = "${ApiEndpoints.updateContact}/$contactId"; logSafe("Updating contact [$contactId] with payload: $payload"); final response = await _putRequest(endpoint, payload); if (response != null) { final json = jsonDecode(response.body); if (json['success'] == true) { logSafe("Contact updated successfully."); return true; } else { logSafe("Update contact failed: ${json['message']}", level: LogLevel.warning); } } } catch (e) { logSafe("Error updating contact: $e", level: LogLevel.error); } return false; } static Future createContact(Map payload) async { try { logSafe("Submitting contact payload: $payload"); final response = await _postRequest(ApiEndpoints.createContact, payload); if (response != null) { final json = jsonDecode(response.body); if (json['success'] == true) { logSafe("Contact created successfully."); return true; } else { logSafe("Create contact failed: ${json['message']}", level: LogLevel.warning); } } } catch (e) { logSafe("Error creating contact: $e", level: LogLevel.error); } return false; } static Future> getOrganizationList() async { try { final url = ApiEndpoints.getDirectoryOrganization; logSafe("Sending GET request to: $url", level: LogLevel.info); final response = await _getRequest(url); logSafe("Response status: ${response?.statusCode}", level: LogLevel.debug); logSafe("Response body: ${response?.body}", level: LogLevel.debug); if (response != null && response.statusCode == 200) { final body = jsonDecode(response.body); if (body['success'] == true && body['data'] is List) { return List.from(body['data']); } } } catch (e, stackTrace) { logSafe("Failed to fetch organization names: $e", level: LogLevel.error); logSafe("Stack trace: $stackTrace", level: LogLevel.debug); } return []; } static Future?> getContactCategoryList() async => _getRequest(ApiEndpoints.getDirectoryContactCategory).then((res) => res != null ? _parseResponseForAllData(res, label: 'Contact Category List') : null); static Future?> getContactTagList() async => _getRequest(ApiEndpoints.getDirectoryContactTags).then((res) => res != null ? _parseResponseForAllData(res, label: 'Contact Tag List') : null); static Future?> getDirectoryData( {required bool isActive}) async { final queryParams = { "active": isActive.toString(), }; return _getRequest(ApiEndpoints.getDirectoryContacts, queryParams: queryParams) .then((res) => res != null ? _parseResponse(res, label: 'Directory Data') : null); } static Future?> getContactBucketList() async => _getRequest(ApiEndpoints.getDirectoryBucketList).then((res) => res != null ? _parseResponseForAllData(res, label: 'Contact Bucket List') : null); // === Attendance APIs === static Future?> getProjects() async => _getRequest(ApiEndpoints.getProjects).then( (res) => res != null ? _parseResponse(res, label: 'Projects') : null); static Future?> getGlobalProjects() async => _getRequest(ApiEndpoints.getGlobalProjects).then((res) => res != null ? _parseResponse(res, label: 'Global Projects') : null); static Future?> getEmployeesByProject(String projectId) async => _getRequest(ApiEndpoints.getEmployeesByProject, queryParams: {"projectId": projectId}) .then((res) => res != null ? _parseResponse(res, label: 'Employees') : null); static Future?> getAttendanceLogs( String projectId, { DateTime? dateFrom, DateTime? dateTo, }) 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), }; 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?> getRegularizationLogs(String projectId) async => _getRequest(ApiEndpoints.getRegularizationLogs, queryParams: {"projectId": projectId}) .then((res) => res != null ? _parseResponse(res, label: 'Regularization Logs') : 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, String? markTime, }) async { final now = DateTime.now(); final body = { "id": id, "employeeId": employeeId, "projectId": projectId, "markTime": markTime ?? DateFormat('hh:mm a').format(now), "comment": comment, "action": action, "date": DateFormat('yyyy-MM-dd').format(now), if (imageCapture) "latitude": '$latitude', if (imageCapture) "longitude": '$longitude', }; if (imageCapture && imageFile != null) { try { final bytes = await imageFile.readAsBytes(); final fileSize = await imageFile.length(); final contentType = "image/${imageFile.path.split('.').last}"; body["image"] = { "fileName": imageName, "contentType": contentType, "fileSize": fileSize, "description": "Employee attendance photo", "base64Data": base64Encode(bytes), }; } catch (e) { logSafe("Image encoding error: $e", level: LogLevel.error); return false; } } final response = await _postRequest( ApiEndpoints.uploadAttendanceImage, body, customTimeout: extendedTimeout, ); if (response == null) return false; final json = jsonDecode(response.body); if (response.statusCode == 200 && json['success'] == true) return true; logSafe("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); return false; } static String generateImageName(String employeeId, int count) { final now = DateTime.now(); final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now); final imageNumber = count.toString().padLeft(3, '0'); return "${employeeId}_${dateStr}_$imageNumber.jpg"; } // === Employee APIs === static Future?> getAllEmployeesByProject( String projectId) async { if (projectId.isEmpty) throw ArgumentError('projectId must not be empty'); final endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId"; return _getRequest(endpoint).then((res) => res != null ? _parseResponse(res, label: 'Employees by Project') : null); } static Future?> getAllEmployees() async => _getRequest(ApiEndpoints.getAllEmployees).then((res) => res != null ? _parseResponse(res, label: 'All Employees') : null); static Future?> getRoles() async => _getRequest(ApiEndpoints.getRoles).then( (res) => res != null ? _parseResponse(res, label: 'Roles') : null); static Future createEmployee({ required String firstName, required String lastName, required String phoneNumber, required String gender, required String jobRoleId, }) async { final body = { "firstName": firstName, "lastName": lastName, "phoneNumber": phoneNumber, "gender": gender, "jobRoleId": jobRoleId, }; final response = await _postRequest( ApiEndpoints.createEmployee, body, customTimeout: extendedTimeout, ); if (response == null) return false; final json = jsonDecode(response.body); return response.statusCode == 200 && json['success'] == true; } static Future?> getEmployeeDetails( String employeeId) async { final url = "${ApiEndpoints.getEmployeeInfo}/$employeeId"; final response = await _getRequest(url); final data = response != null ? _parseResponse(response, label: 'Employee Details') : null; return data is Map ? data : null; } // === Daily Task APIs === static Future?> getDailyTasks( String projectId, { DateTime? dateFrom, DateTime? dateTo, }) 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), }; return _getRequest(ApiEndpoints.getDailyTask, queryParams: query).then( (res) => res != null ? _parseResponse(res, label: 'Daily Tasks') : null); } static Future reportTask({ required String id, required int completedTask, required String comment, required List> checkList, List>? images, }) async { final body = { "id": id, "completedTask": completedTask, "comment": comment, "reportedDate": DateTime.now().toUtc().toIso8601String(), "checkList": checkList, if (images != null && images.isNotEmpty) "images": images, }; final response = await _postRequest( ApiEndpoints.reportTask, body, customTimeout: extendedTimeout, ); if (response == null) return false; final json = jsonDecode(response.body); if (response.statusCode == 200 && json['success'] == true) { Get.back(); return true; } logSafe("Failed to report task: ${json['message'] ?? 'Unknown error'}"); return false; } static Future commentTask({ required String id, required String comment, List>? images, }) async { final body = { "taskAllocationId": id, "comment": comment, "commentDate": DateTime.now().toUtc().toIso8601String(), if (images != null && images.isNotEmpty) "images": images, }; final response = await _postRequest(ApiEndpoints.commentTask, body); if (response == null) return false; final json = jsonDecode(response.body); return response.statusCode == 200 && json['success'] == true; } static Future?> getDailyTasksDetails( String projectId) async { final url = "${ApiEndpoints.dailyTaskDetails}/$projectId"; final response = await _getRequest(url); return response != null ? _parseResponseForAllData(response, label: 'Daily Task Details') as Map? : null; } static Future assignDailyTask({ required String workItemId, required int plannedTask, required String description, required List taskTeam, DateTime? assignmentDate, }) async { final body = { "workItemId": workItemId, "plannedTask": plannedTask, "description": description, "taskTeam": taskTeam, "assignmentDate": (assignmentDate ?? DateTime.now()).toUtc().toIso8601String(), }; final response = await _postRequest(ApiEndpoints.assignDailyTask, body); if (response == null) return false; final json = jsonDecode(response.body); if (response.statusCode == 200 && json['success'] == true) { Get.back(); return true; } logSafe( "Failed to assign daily task: ${json['message'] ?? 'Unknown error'}"); return false; } static Future?> getWorkStatus() async { final res = await _getRequest(ApiEndpoints.getWorkStatus); if (res == null) { logSafe('Work Status API returned null'); return null; } logSafe('Work Status raw response: ${res.body}'); return _parseResponseForAllData(res, label: 'Work Status') as Map?; } static Future?> getMasterWorkCategories() async => _getRequest(ApiEndpoints.getmasterWorkCategories).then((res) => res != null ? _parseResponseForAllData(res, label: 'Master Work Categories') : null); static Future approveTask({ required String id, required String comment, required String workStatus, required int approvedTask, List>? images, }) async { final body = { "id": id, "workStatus": workStatus, "approvedTask": approvedTask, "comment": comment, if (images != null && images.isNotEmpty) "images": images, }; final response = await _postRequest(ApiEndpoints.approveReportAction, body); if (response == null) return false; final json = jsonDecode(response.body); return response.statusCode == 200 && json['success'] == true; } static Future createTask({ required String parentTaskId, required int plannedTask, required String comment, required String workAreaId, required String activityId, DateTime? assignmentDate, required String categoryId, }) async { final body = [ { "parentTaskId": parentTaskId, "plannedWork": plannedTask, "comment": comment, "workAreaID": workAreaId, "activityID": activityId, "workCategoryId": categoryId, 'completedWork': 0, } ]; final response = await _postRequest(ApiEndpoints.assignTask, body); if (response == null) return false; final json = jsonDecode(response.body); if (response.statusCode == 200 && json['success'] == true) { Get.back(); return true; } logSafe("Failed to create task: ${json['message'] ?? 'Unknown error'}"); return false; } }