2658 lines
82 KiB
Dart

import 'dart:convert';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
// Helper Services
import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/api_endpoints.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/utils/encryption_helper.dart';
// Models
import 'package:on_field_work/model/dashboard/project_progress_model.dart';
import 'package:on_field_work/model/dashboard/dashboard_tasks_model.dart';
import 'package:on_field_work/model/dashboard/dashboard_teams_model.dart';
import 'package:on_field_work/model/document/document_filter_model.dart';
import 'package:on_field_work/model/document/documents_list_model.dart';
import 'package:on_field_work/model/document/master_document_tags.dart';
import 'package:on_field_work/model/document/master_document_type_model.dart';
import 'package:on_field_work/model/document/document_details_model.dart';
import 'package:on_field_work/model/document/document_version_model.dart';
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
import 'package:on_field_work/model/tenant/tenant_services_model.dart';
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_model.dart';
import 'package:on_field_work/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
import 'package:on_field_work/model/all_organization_model.dart';
import 'package:on_field_work/model/dashboard/pending_expenses_model.dart';
import 'package:on_field_work/model/dashboard/expense_type_report_model.dart';
import 'package:on_field_work/model/dashboard/monthly_expence_model.dart';
import 'package:on_field_work/model/finance/expense_category_model.dart';
import 'package:on_field_work/model/finance/currency_list_model.dart';
import 'package:on_field_work/model/finance/payment_payee_request_model.dart';
import 'package:on_field_work/model/finance/payment_request_list_model.dart';
import 'package:on_field_work/model/finance/payment_request_filter.dart';
import 'package:on_field_work/model/finance/payment_request_details_model.dart';
import 'package:on_field_work/model/finance/advance_payment_model.dart';
import 'package:on_field_work/model/service_project/service_projects_list_model.dart';
import 'package:on_field_work/model/service_project/service_projects_details_model.dart';
import 'package:on_field_work/model/service_project/job_list_model.dart';
import 'package:on_field_work/model/service_project/service_project_job_detail_model.dart';
import 'package:on_field_work/model/service_project/job_attendance_logs_model.dart';
import 'package:on_field_work/model/service_project/job_allocation_model.dart';
import 'package:on_field_work/model/service_project/service_project_branches_model.dart';
import 'package:on_field_work/model/service_project/job_status_response.dart';
import 'package:on_field_work/model/service_project/job_comments.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/infra_project/infra_project_list.dart';
import 'package:on_field_work/model/infra_project/infra_project_details.dart';
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
class ApiService {
static const bool enableLogs = true;
static const Duration extendedTimeout = Duration(seconds: 60);
static void _log(String message, {LogLevel level = LogLevel.info}) {
if (enableLogs) {
logSafe(message, level: level);
}
}
/// Utility to get the token, handle expiry, and refresh if needed.
static Future<String?> _getToken() async {
final token = LocalStorage.getJwtToken();
if (token == null) {
_log("No JWT token found. Logging out...", level: LogLevel.error);
await LocalStorage.logout();
return null;
}
try {
final expirationDate = JwtDecoder.getExpirationDate(token);
final difference = expirationDate.difference(DateTime.now());
if (JwtDecoder.isExpired(token) || difference.inMinutes < 2) {
_log(
"Token expired or near expiry (${difference.inSeconds}s). Attempting refresh...",
level: LogLevel.warning,
);
final refreshed = await AuthService.refreshToken();
if (refreshed) {
return LocalStorage.getJwtToken();
} else {
_log("Token refresh failed. Logging out.", level: LogLevel.error);
await LocalStorage.logout();
return null;
}
}
} catch (e) {
_log("Token decoding error: $e", level: LogLevel.error);
await LocalStorage.logout();
return null;
}
return token;
}
static Map<String, String> _headers(String token) => {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
};
// --- Centralized Response Parsing and Decryption ---
/// Handles decryption, status code checks, and returns the 'data' payload or the full JSON.
static dynamic _parseAndDecryptResponse(http.Response response,
{String label = '', bool returnFullResponse = false}) {
final body = response.body.trim();
_log("$label Encrypted Response (Status ${response.statusCode}): $body",
level: LogLevel.debug);
if (body.isEmpty) {
_log("Empty response body for [$label]");
return null;
}
// Decrypt the Base64 string and auto-decode JSON
final decryptedJson = decryptResponse(body);
if (decryptedJson == null) {
_log("Decryption failed for [$label]. Cannot parse response.",
level: LogLevel.error);
return null;
}
// Handle non-200 or failure scenarios
if (response.statusCode != 200 ||
decryptedJson is! Map ||
decryptedJson['success'] != true) {
final message = decryptedJson is Map
? decryptedJson['message'] ?? 'Unknown error'
: 'Invalid decrypted response format.';
_log("API Error [$label] (Status ${response.statusCode}): $message",
level: LogLevel.warning);
return null;
}
_log(
"$label Decrypted Data: ${decryptedJson['data'] ?? 'Success (No data field)'}");
return returnFullResponse ? decryptedJson : decryptedJson['data'];
}
// --- Generic Request Execution Layer ---
/// Executes any HTTP request (GET, POST, PUT, DELETE, PATCH) safely with token management and retry logic.
static Future<http.Response?> _safeApiCall(
String endpoint, {
String method = 'GET',
Map<String, String>? queryParams,
dynamic body,
Map<String, String>? additionalHeaders,
Duration customTimeout = extendedTimeout,
bool hasRetried = false,
}) async {
String? token = await _getToken();
if (token == null) {
_log("Token is null. Cannot proceed with $method request.",
level: LogLevel.error);
return null;
}
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint").replace(
queryParameters: queryParams,
);
final headers = {
..._headers(token),
if (additionalHeaders != null) ...additionalHeaders,
};
_log("Initiating $method request to $uri", level: LogLevel.debug);
if (body != null) {
_log("Request Body: ${body is String ? body : jsonEncode(body)}",
level: LogLevel.debug);
}
try {
http.Response response;
final encodedBody = (body != null) ? jsonEncode(body) : null;
switch (method) {
case 'GET':
response =
await http.get(uri, headers: headers).timeout(customTimeout);
break;
case 'POST':
response = await http
.post(uri, headers: headers, body: encodedBody)
.timeout(customTimeout);
break;
case 'PUT':
response = await http
.put(uri, headers: headers, body: encodedBody)
.timeout(customTimeout);
break;
case 'DELETE':
response =
await http.delete(uri, headers: headers).timeout(customTimeout);
break;
case 'PATCH':
response = await http
.patch(uri, headers: headers, body: encodedBody)
.timeout(customTimeout);
break;
default:
throw UnsupportedError("Unsupported HTTP method: $method");
}
_log("$method Response Status: ${response.statusCode}",
level: LogLevel.debug);
if (response.statusCode == 401 && !hasRetried) {
_log("Unauthorized (401). Attempting token refresh and retry...",
level: LogLevel.warning);
if (await AuthService.refreshToken()) {
return await _safeApiCall(
endpoint,
method: method,
queryParams: queryParams,
body: body,
additionalHeaders: additionalHeaders,
customTimeout: customTimeout,
hasRetried: true, // Prevent infinite loop
);
}
_log("Token refresh failed on 401. Logging out user.",
level: LogLevel.error);
await LocalStorage.logout();
}
return response;
} catch (e, stack) {
_log("HTTP $method Exception: $e\n$stack", level: LogLevel.error);
return null;
}
}
// --- Public API Methods (Utilizing Generic Call) ---
/// ============================================
/// DASHBOARD
/// ============================================
static Future<PurchaseInvoiceOverviewResponse?> getPurchaseInvoiceOverview({
String? projectId,
}) async {
final queryParams = <String, String>{};
if (projectId != null && projectId.isNotEmpty) {
queryParams['projectId'] = projectId;
}
final response = await _safeApiCall(
ApiEndpoints.getPurchaseInvoiceOverview,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
final parsedJson = _parseAndDecryptResponse(
response,
label: "PurchaseInvoiceOverview",
returnFullResponse: true,
);
if (parsedJson == null) return null;
return PurchaseInvoiceOverviewResponse.fromJson(parsedJson);
}
static Future<CollectionOverviewResponse?> getCollectionOverview({
String? projectId,
}) async {
final queryParams = <String, String>{};
if (projectId != null && projectId.isNotEmpty) {
queryParams['projectId'] = projectId;
}
final response = await _safeApiCall(
ApiEndpoints.getCollectionOverview,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
final parsedJson = _parseAndDecryptResponse(response,
label: "CollectionOverview", returnFullResponse: true);
if (parsedJson == null) return null;
return CollectionOverviewResponse.fromJson(parsedJson);
}
static Future<DashboardTasks?> getDashboardTasks(
{required String projectId}) async {
final response = await _safeApiCall(
ApiEndpoints.getDashboardTasks,
method: 'GET',
queryParams: {'projectId': projectId},
);
if (response == null) return null;
final jsonResponse = _parseAndDecryptResponse(response,
label: "DashboardTasks", returnFullResponse: true);
if (jsonResponse == null) return null;
return DashboardTasks.fromJson(jsonResponse);
}
static Future<DashboardTeams?> 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<List<dynamic>?> 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<ProjectResponse?> 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<PendingExpensesResponse?> getPendingExpensesApi({
required String projectId,
}) async {
final response = await _safeApiCall(
ApiEndpoints.getPendingExpenses,
method: 'GET',
queryParams: {'projectId': projectId},
);
if (response == null) return null;
final jsonResponse = _parseAndDecryptResponse(response,
label: "Pending Expenses", returnFullResponse: true);
if (jsonResponse == null) return null;
return PendingExpensesResponse.fromJson(jsonResponse);
}
static Future<ExpenseTypeReportResponse?> 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<DashboardMonthlyExpenseResponse?>
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<ProjectsResponse?> getInfraProjectsList({
int pageSize = 20,
int pageNumber = 1,
String searchString = "",
}) async {
final queryParams = {
"pageSize": pageSize.toString(),
"pageNumber": pageNumber.toString(),
"searchString": searchString,
};
final response = await _safeApiCall(
ApiEndpoints.getInfraProjectsList,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
final parsedJson = _parseAndDecryptResponse(response,
label: "InfraProjectsList", returnFullResponse: true);
if (parsedJson == null) return null;
return ProjectsResponse.fromJson(parsedJson);
}
static Future<ProjectDetailsResponse?> 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<ServiceProjectListModel?> getServiceProjectsListApi({
int pageNumber = 1,
int pageSize = 20,
}) async {
final queryParams = {
'pageNumber': pageNumber.toString(),
'pageSize': pageSize.toString(),
};
final response = await _safeApiCall(
ApiEndpoints.getServiceProjectsList,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
final jsonResponse = _parseAndDecryptResponse(response,
label: "Service Project List", returnFullResponse: true);
if (jsonResponse == null) return null;
return ServiceProjectListModel.fromJson(jsonResponse);
}
static Future<ServiceProjectDetailModel?> getServiceProjectDetailApi(
String projectId) async {
final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId";
final response = await _safeApiCall(endpoint, method: 'GET');
if (response == null) return null;
final jsonResponse = _parseAndDecryptResponse(response,
label: "Service Project Detail", returnFullResponse: true);
if (jsonResponse == null) return null;
return ServiceProjectDetailModel.fromJson(jsonResponse);
}
static Future<JobResponse?> getServiceProjectJobListApi({
required String projectId,
int pageNumber = 1,
int pageSize = 20,
bool isActive = true,
bool isArchive = false,
}) async {
final queryParams = {
'projectId': projectId,
'pageNumber': pageNumber.toString(),
'pageSize': pageSize.toString(),
'isActive': isActive.toString(),
if (isArchive) 'isArchive': 'true',
};
final response = await _safeApiCall(
ApiEndpoints.getServiceProjectJobList,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
final jsonResponse = _parseAndDecryptResponse(response,
label: isArchive
? "Archived Service Project Job List"
: "Active Service Project Job List",
returnFullResponse: true);
if (jsonResponse == null) return null;
return JobResponse.fromJson(jsonResponse);
}
static Future<String?> createServiceProjectJobApi({
required String title,
required String description,
required String projectId,
required List<Map<String, dynamic>> assignees,
required DateTime startDate,
required DateTime dueDate,
required List<Map<String, dynamic>> 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<JobDetailsResponse?> 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<bool> editServiceProjectJobApi({
required String jobId,
required List<Map<String, dynamic>> 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<List<dynamic>?> getEmployees({
Map<String, String>? 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<bool> updateServiceProjectJobAttendance({
required Map<String, dynamic> 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<JobAttendanceResponse?> 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<List<ServiceProjectAllocation>?>
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<bool> manageServiceProjectAllocation({
required List<Map<String, dynamic>> 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<List<TeamRole>?> 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<String, dynamic>))
.toList();
}
return null;
}
static Future<ServiceProjectBranchesResponse?> getServiceProjectBranchesFull({
required String projectId,
int pageNumber = 1,
int pageSize = 20,
String searchString = '',
bool isActive = true,
}) async {
final queryParams = {
'pageNumber': pageNumber.toString(),
'pageSize': pageSize.toString(),
'searchString': searchString,
'isActive': isActive.toString(),
};
final endpoint = "${ApiEndpoints.getServiceProjectBranches}/$projectId";
final response = await _safeApiCall(
endpoint,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
final parsedJson = _parseAndDecryptResponse(
response,
label: "ServiceProjectBranchesFull",
returnFullResponse: true,
);
if (parsedJson == null) return null;
return ServiceProjectBranchesResponse.fromJson(parsedJson);
}
static Future<List<JobStatus>?> getMasterJobStatus({
required String statusId,
required String projectId,
}) async {
final queryParams = {
'statusId': statusId,
'projectId': projectId,
};
final response = await _safeApiCall(
ApiEndpoints.getMasterJobStatus,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
final data = _parseAndDecryptResponse(response, label: "MasterJobStatus");
if (data is List) {
return data.map((e) => JobStatus.fromJson(e)).toList();
}
return null;
}
static Future<bool> addJobComment({
required String jobTicketId,
required String comment,
List<Map<String, dynamic>> attachments = const [],
}) async {
final body = {
"jobTicketId": jobTicketId,
"comment": comment,
"attachments": attachments,
};
final response = await _safeApiCall(
ApiEndpoints.addJobComment,
method: 'POST',
body: body,
);
if (response == null) return false;
// Check status 201 for success, or rely on decrypted 'success' flag
final parsed = _parseAndDecryptResponse(response,
label: "AddJobComment", returnFullResponse: true);
return response.statusCode == 201 ||
(parsed != null && parsed['success'] == true);
}
static Future<JobCommentResponse?> getJobCommentList({
required String jobTicketId,
int pageNumber = 1,
int pageSize = 20,
}) async {
final queryParams = {
'jobTicketId': jobTicketId,
'pageNumber': pageNumber.toString(),
'pageSize': pageSize.toString(),
};
final response = await _safeApiCall(
ApiEndpoints.getJobCommentList,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
final parsedJson = _parseAndDecryptResponse(response,
label: "JobCommentList", returnFullResponse: true);
if (parsedJson == null) return null;
return JobCommentResponse.fromJson(parsedJson);
}
/// ============================================
/// FINANCE & EXPENSE
/// ============================================
static Future<PaymentRequestPayeeResponse?>
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<ExpenseCategoryResponse?>
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<CurrencyListResponse?> 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<bool> 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<Map<String, dynamic>> 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<PaymentRequestResponse?> getExpensePaymentRequestListApi({
bool isActive = true,
int pageSize = 20,
int pageNumber = 1,
Map<String, dynamic>? 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<PaymentRequestFilter?>
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<PaymentRequestDetail?> 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<bool> 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<bool> createExpenseForPRApi({
required String paymentModeId,
required String location,
required String gstNumber,
required String statusId,
required String paymentRequestId,
required String comment,
List<Map<String, dynamic>> billAttachments = const [],
}) async {
final body = {
"paymentModeId": paymentModeId,
"location": location,
"gstNumber": gstNumber,
"statusId": statusId,
"comment": comment,
"paymentRequestId": paymentRequestId,
"billAttachments": billAttachments,
};
final response = await _safeApiCall(
ApiEndpoints.createExpenseforPR,
method: 'POST',
body: body,
);
if (response == null) return false;
final parsed = _parseAndDecryptResponse(response,
label: "Create Expense for PR", returnFullResponse: true);
return parsed != null && parsed['success'] == true;
}
static Future<bool> editExpensePaymentRequestApi({
required String id,
required String title,
required String description,
required String payee,
required String currencyId,
required double amount,
required String dueDate,
required String projectId,
required String expenseCategoryId,
required bool isAdvancePayment,
List<Map<String, dynamic>> billAttachments = const [],
}) async {
final endpoint = "${ApiEndpoints.getExpensePaymentRequestEdit}/$id";
final body = {
"id": id,
"title": title,
"description": description,
"payee": payee,
"currencyId": currencyId,
"amount": amount,
"dueDate": dueDate,
"projectId": projectId,
"expenseCategoryId": expenseCategoryId,
"isAdvancePayment": isAdvancePayment,
"billAttachments": billAttachments,
};
final response = await _safeApiCall(
endpoint,
method: 'PUT',
body: body,
);
if (response == null) return false;
final parsed = _parseAndDecryptResponse(response,
label: "Edit Expense PR", returnFullResponse: true);
return parsed != null && parsed['success'] == true;
}
static Future<List<AdvancePayment>> getAdvancePayments(
String employeeId) async {
final endpoint = "${ApiEndpoints.getAdvancePayments}/$employeeId";
final response = await _safeApiCall(endpoint, method: 'GET');
if (response == null) return [];
final data = _parseAndDecryptResponse(response, label: 'Advance Payments');
if (data is List) {
return data.map((e) => AdvancePayment.fromJson(e)).toList();
}
return [];
}
static Future<List<dynamic>?> getMasterPaymentModes() async {
final response =
await _safeApiCall(ApiEndpoints.getMasterPaymentModes, method: 'GET');
if (response == null) return null;
return _parseAndDecryptResponse(response, label: 'Master Payment Modes');
}
static Future<List<dynamic>?> getMasterExpenseStatus() async {
final response =
await _safeApiCall(ApiEndpoints.getMasterExpenseStatus, method: 'GET');
if (response == null) return null;
return _parseAndDecryptResponse(response, label: 'Master Expense Status');
}
static Future<List<dynamic>?> getMasterExpenseTypes() async {
final response = await _safeApiCall(ApiEndpoints.getMasterExpenseCategory,
method: 'GET');
if (response == null) return null;
return _parseAndDecryptResponse(response,
label: 'Master Expense Categorys');
}
static Future<bool> createExpenseApi({
required String projectId,
required String expensesTypeId,
required String paymentModeId,
required String paidById,
required DateTime transactionDate,
required String transactionId,
required String description,
required String location,
required String supplerName,
required double amount,
required int noOfPersons,
required List<Map<String, dynamic>> billAttachments,
}) async {
final payload = {
"projectId": projectId,
"expenseCategoryId": expensesTypeId,
"paymentModeId": paymentModeId,
"paidById": paidById,
"transactionDate": transactionDate.toIso8601String(),
"transactionId": transactionId,
"description": description,
"location": location,
"supplerName": supplerName,
"amount": amount,
"noOfPersons": noOfPersons,
"billAttachments": billAttachments,
};
final response = await _safeApiCall(
ApiEndpoints.createExpense,
method: 'POST',
body: payload,
);
if (response == null) return false;
final parsed = _parseAndDecryptResponse(response,
label: "Create Expense", returnFullResponse: true);
return parsed != null && parsed['success'] == true;
}
static Future<Map<String, dynamic>?> getExpenseDetailsApi({
required String expenseId,
}) async {
final endpoint = "${ApiEndpoints.getExpenseDetails}/$expenseId";
final response = await _safeApiCall(endpoint, method: 'GET');
if (response == null) return null;
// NOTE: This API currently does NOT return encrypted response based on original implementation.
// Assuming the decrypted response returns the whole map containing 'data'
final jsonResponse = _parseAndDecryptResponse(response,
label: "Expense Details", returnFullResponse: true);
return (jsonResponse != null &&
jsonResponse['data'] is Map<String, dynamic>)
? jsonResponse['data']
: null;
}
static Future<bool> updateExpenseStatusApi({
required String expenseId,
required String statusId,
String? comment,
String? reimburseTransactionId,
String? reimburseDate,
String? reimbursedById,
double? baseAmount,
double? taxAmount,
double? tdsPercent,
double? netPayable,
}) async {
final payload = {
"expenseId": expenseId,
"statusId": statusId,
if (comment != null) "comment": comment,
if (reimburseTransactionId != null)
"reimburseTransactionId": reimburseTransactionId,
if (reimburseDate != null) "reimburseDate": reimburseDate,
if (reimbursedById != null) "reimburseById": reimbursedById,
if (baseAmount != null) "baseAmount": baseAmount,
if (taxAmount != null) "taxAmount": taxAmount,
if (tdsPercent != null) "tdsPercent": tdsPercent,
if (netPayable != null) "netPayable": netPayable,
};
final response = await _safeApiCall(
ApiEndpoints.updateExpenseStatus,
method: 'POST',
body: payload,
);
if (response == null) return false;
final parsed = _parseAndDecryptResponse(response,
label: "Update Expense Status", returnFullResponse: true);
return parsed != null && parsed['success'] == true;
}
static Future<bool> editExpenseApi({
required String expenseId,
required Map<String, dynamic> payload,
}) async {
final endpoint = "${ApiEndpoints.editExpense}/$expenseId";
final response = await _safeApiCall(
endpoint,
method: 'PUT',
body: payload,
);
if (response == null) return false;
final parsed = _parseAndDecryptResponse(response,
label: "Edit Expense", returnFullResponse: true);
return parsed != null && parsed['success'] == true;
}
static Future<bool> deleteExpense(String expenseId) async {
final endpoint = "${ApiEndpoints.deleteExpense}/$expenseId";
final response = await _safeApiCall(endpoint, method: 'DELETE');
if (response == null) return false;
// DELETE requests often return 200 with a success body, or 204 (No Content).
// Rely on parse function which checks for 200 and 'success: true'
final parsed = _parseAndDecryptResponse(response,
label: "Delete Expense", returnFullResponse: true);
return response.statusCode == 204 ||
(parsed != null && parsed['success'] == true);
}
static Future<Map<String, dynamic>?> getExpenseListApi({
String? filter,
int pageSize = 20,
int pageNumber = 1,
}) async {
final queryParams = {
'pageSize': pageSize.toString(),
'pageNumber': pageNumber.toString(),
if (filter?.isNotEmpty ?? false) 'filter': filter!,
};
final response = await _safeApiCall(
ApiEndpoints.getExpenseList,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
final jsonResponse = _parseAndDecryptResponse(response,
label: "Expense List", returnFullResponse: true);
return jsonResponse is Map<String, dynamic> ? jsonResponse : null;
}
/// ============================================
/// DOCUMENT MANAGEMENT
/// ============================================
static Future<DocumentFiltersResponse?> getDocumentFilters(
String entityTypeId) async {
final endpoint = "${ApiEndpoints.getDocumentFilter}/$entityTypeId";
final response = await _safeApiCall(endpoint, method: 'GET');
if (response == null) return null;
final jsonResponse = _parseAndDecryptResponse(response,
label: "Document Filters", returnFullResponse: true);
if (jsonResponse == null) return null;
return DocumentFiltersResponse.fromJson(jsonResponse);
}
static Future<DocumentsResponse?> getDocumentListApi({
required String entityTypeId,
required String entityId,
String filter = "",
String searchString = "",
bool isActive = true,
int pageNumber = 1,
int pageSize = 20,
}) async {
final endpoint =
"${ApiEndpoints.getDocumentList}/$entityTypeId/entity/$entityId";
final queryParams = {
"filter": filter,
"searchString": searchString,
"isActive": isActive.toString(),
"pageNumber": pageNumber.toString(),
"pageSize": pageSize.toString(),
};
final response =
await _safeApiCall(endpoint, method: 'GET', queryParams: queryParams);
if (response == null) return null;
final jsonResponse = _parseAndDecryptResponse(response,
label: "Document List", returnFullResponse: true);
if (jsonResponse == null) return null;
return DocumentsResponse.fromJson(jsonResponse);
}
static Future<TagResponse?> getMasterDocumentTagsApi() async {
final response =
await _safeApiCall(ApiEndpoints.getMasterDocumentTags, method: 'GET');
if (response == null) return null;
final jsonResponse = _parseAndDecryptResponse(response,
label: "Master Document Tags", returnFullResponse: true);
if (jsonResponse == null) return null;
return TagResponse.fromJson(jsonResponse);
}
static Future<bool> 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<Map<String, dynamic>> tags = const [],
}) async {
final payload = {
"name": name,
"documentId": documentId ?? "",
"description": description ?? "",
"entityId": entityId,
"documentTypeId": documentTypeId,
"attachment": {
"fileName": fileName,
"base64Data": base64Data,
"contentType": contentType,
"fileSize": fileSize,
"description": fileDescription ?? "",
"isActive": isActive,
},
"tags": tags.isNotEmpty
? tags
: [
{"name": "default", "isActive": true}
],
};
final response = await _safeApiCall(
ApiEndpoints.uploadDocument,
method: 'POST',
body: payload,
);
if (response == null) return false;
final parsed = _parseAndDecryptResponse(response,
label: "Upload Document", returnFullResponse: true);
return parsed != null && parsed['success'] == true;
}
static Future<DocumentTypesResponse?> 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<DocumentTypesResponse?> 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<DocumentDetailsResponse?> 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<DocumentVersionsResponse?> 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<bool> editDocumentApi({
required String id,
required String name,
required String documentId,
String? description,
List<Map<String, dynamic>> tags = const [],
Map<String, dynamic>? 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<bool> 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<String?> 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<bool> 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<List<dynamic>?> allEmployeesBasic({
bool allEmployee = true,
}) async {
final response = await _safeApiCall(
ApiEndpoints.getEmployeesWithoutPermission,
method: 'GET',
queryParams: {'allEmployee': allEmployee.toString()},
);
if (response == null) return null;
return _parseAndDecryptResponse(response, label: 'All Employees Basic');
}
static Future<List<dynamic>?> searchEmployeesBasic({
String? searchString,
}) async {
final queryParams = <String, String>{};
if (searchString != null && searchString.isNotEmpty) {
queryParams['searchString'] = searchString;
}
final response = await _safeApiCall(
ApiEndpoints.getEmployeesWithoutPermission,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
return _parseAndDecryptResponse(response, label: 'Search Employees Basic');
}
static Future<List<dynamic>?> getAllEmployeesByProject(String projectId,
{String? organizationId}) async {
if (projectId.isEmpty) throw ArgumentError('projectId must not be empty');
final endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId";
final queryParams = <String, String>{
if (organizationId != null && organizationId.isNotEmpty)
"organizationId": organizationId
};
final response = await _safeApiCall(
endpoint,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
return _parseAndDecryptResponse(response, label: 'Employees by Project');
}
static Future<List<dynamic>?> getEmployeesByProjectService(
String projectId, {
String? serviceId,
String? organizationId,
}) async {
if (projectId.isEmpty) throw ArgumentError('projectId must not be empty');
final queryParams = <String, String>{};
if (serviceId != null && serviceId.isNotEmpty) {
queryParams['serviceId'] = serviceId;
}
if (organizationId != null && organizationId.isNotEmpty) {
queryParams['organizationId'] = organizationId;
}
final endpoint = "${ApiEndpoints.getAllEmployeesByOrganization}/$projectId";
final response = await _safeApiCall(
endpoint,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
return _parseAndDecryptResponse(response,
label: 'Employees by Project Service');
}
static Future<List<dynamic>?> getAllEmployees(
{String? organizationId}) async {
final queryParams = <String, String>{
if (organizationId != null && organizationId.isNotEmpty)
"organizationId": organizationId
};
final response = await _safeApiCall(
ApiEndpoints.getAllEmployees,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
return _parseAndDecryptResponse(response, label: 'All Employees');
}
static Future<List<dynamic>?> getRoles() async {
final response = await _safeApiCall(ApiEndpoints.getRoles, method: 'GET');
if (response == null) return null;
return _parseAndDecryptResponse(response, label: 'Roles');
}
static Future<Map<String, dynamic>?> createEmployee({
String? id,
required String firstName,
required String lastName,
required String phoneNumber,
required String gender,
required String jobRoleId,
required String joiningDate,
String? email,
String? organizationId,
bool? hasApplicationAccess,
}) async {
final body = {
if (id != null) "id": id,
"firstName": firstName,
"lastName": lastName,
"phoneNumber": phoneNumber,
"gender": gender,
"jobRoleId": jobRoleId,
"joiningDate": joiningDate,
if (email != null && email.isNotEmpty) "email": email,
if (organizationId != null && organizationId.isNotEmpty)
"organizationId": organizationId,
if (hasApplicationAccess != null)
"hasApplicationAccess": hasApplicationAccess,
};
final response = await _safeApiCall(
ApiEndpoints.createEmployee,
method: 'POST',
body: body,
);
if (response == null) return null;
final json =
jsonDecode(response.body); // Non-encrypted body expected for this path?
// Assuming we need to check the raw JSON status since the model is unclear
return {
"success": response.statusCode == 200 && (json['success'] == true),
"data": json,
};
}
static Future<Map<String, dynamic>?> getEmployeeDetails(
String employeeId) async {
final url = "${ApiEndpoints.getEmployeeInfo}/$employeeId";
final response = await _safeApiCall(url, method: 'GET');
if (response == null) return null;
final data = _parseAndDecryptResponse(response, label: 'Employee Details');
return data is Map<String, dynamic> ? data : null;
}
static Future<List<dynamic>?> 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<bool> assignProjects({
required String employeeId,
required List<Map<String, dynamic>> 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<List<dynamic>?> 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<bool> manageOrganizationHierarchy({
required String employeeId,
required List<Map<String, dynamic>> 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<bool> 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<OrganizationListResponse?> 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<AllOrganizationListResponse?> 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<ServiceListResponse?> 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<DailyProgressReportFilterResponse?> getDailyTaskFilter(
String projectId) async {
final endpoint =
"${ApiEndpoints.getDailyTaskProjectProgressFilter}/$projectId";
final response = await _safeApiCall(endpoint, method: 'GET');
if (response == null) return null;
final jsonResponse = _parseAndDecryptResponse(response,
label: "Daily Task Progress Filter", returnFullResponse: true);
if (jsonResponse == null) return null;
return DailyProgressReportFilterResponse.fromJson(jsonResponse);
}
static Future<List<TaskModel>?> getDailyTasks(
String projectId, {
Map<String, dynamic>? filter,
int pageNumber = 1,
int pageSize = 20,
}) async {
final query = {
"projectId": projectId,
"pageNumber": pageNumber.toString(),
"pageSize": pageSize.toString(),
if (filter != null) "filter": jsonEncode(filter),
};
final response = await _safeApiCall(
ApiEndpoints.getDailyTask,
method: 'GET',
queryParams: query,
);
if (response == null) return null;
final parsed = _parseAndDecryptResponse(response, label: 'Daily Tasks');
if (parsed != null && parsed is Map && parsed['data'] is List) {
return (parsed['data'] as List)
.map((e) => TaskModel.fromJson(e))
.toList();
}
return null;
}
static Future<bool> reportTask({
required String id,
required int completedTask,
required String comment,
required List<Map<String, dynamic>> checkList,
List<Map<String, dynamic>>? images,
}) async {
final body = {
"id": id,
"completedTask": completedTask,
"comment": comment,
"reportedDate": DateTime.now().toUtc().toIso8601String(),
"checkList": checkList,
if (images != null && images.isNotEmpty) "images": images,
};
final response = await _safeApiCall(
ApiEndpoints.reportTask,
method: 'POST',
body: body,
);
if (response == null) return false;
final parsed = _parseAndDecryptResponse(response,
label: "Report Task", returnFullResponse: true);
if (parsed != null && parsed['success'] == true) {
Get.back(); // Retaining Get.back() as per original logic
return true;
}
return false;
}
static Future<bool> commentTask({
required String id,
required String comment,
List<Map<String, dynamic>>? images,
}) async {
final body = {
"taskAllocationId": id,
"comment": comment,
"commentDate": DateTime.now().toUtc().toIso8601String(),
if (images != null && images.isNotEmpty) "images": images,
};
final response = await _safeApiCall(
ApiEndpoints.commentTask,
method: 'POST',
body: body,
);
if (response == null) return false;
final parsed = _parseAndDecryptResponse(response,
label: "Comment Task", returnFullResponse: true);
return parsed != null && parsed['success'] == true;
}
static Future<Map<String, dynamic>?> getInfraDetails(String projectId,
{String? serviceId}) async {
String endpoint = "/project/infra-details/$projectId";
final queryParams = <String, String>{
if (serviceId != null && serviceId.isNotEmpty) 'serviceId': serviceId,
};
final response = await _safeApiCall(
endpoint,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
return _parseAndDecryptResponse(response,
label: 'Infra Details',
returnFullResponse: true) as Map<String, dynamic>?;
}
static Future<Map<String, dynamic>?> getWorkItemsByWorkArea(String workAreaId,
{String? serviceId}) async {
String endpoint = "/project/tasks/$workAreaId";
final queryParams = <String, String>{
if (serviceId != null && serviceId.isNotEmpty) 'serviceId': serviceId,
};
final response = await _safeApiCall(
endpoint,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
return _parseAndDecryptResponse(response,
label: 'Work Items', returnFullResponse: true) as Map<String, dynamic>?;
}
static Future<bool> assignDailyTask({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
String? organizationId,
String? serviceId,
}) async {
final body = {
"workItemId": workItemId,
"plannedTask": plannedTask,
"description": description,
"taskTeam": taskTeam,
"organizationId": organizationId,
"serviceId": serviceId,
"assignmentDate":
(assignmentDate ?? DateTime.now()).toUtc().toIso8601String(),
};
final response = await _safeApiCall(
ApiEndpoints.assignDailyTask,
method: 'POST',
body: body,
);
if (response == null) return false;
final parsed = _parseAndDecryptResponse(response,
label: "Assign Daily Task", returnFullResponse: true);
if (parsed != null && parsed['success'] == true) {
Get.back(); // Retaining Get.back() as per original logic
return true;
}
return false;
}
static Future<Map<String, dynamic>?> getWorkStatus() async {
final response =
await _safeApiCall(ApiEndpoints.getWorkStatus, method: 'GET');
if (response == null) return null;
return _parseAndDecryptResponse(response,
label: 'Work Status',
returnFullResponse: true) as Map<String, dynamic>?;
}
static Future<Map<String, dynamic>?> 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<String, dynamic>?;
}
static Future<bool> approveTask({
required String id,
required String comment,
required String workStatus,
required int approvedTask,
List<Map<String, dynamic>>? images,
}) async {
final body = {
"id": id,
"workStatus": workStatus,
"approvedTask": approvedTask,
"comment": comment,
if (images != null && images.isNotEmpty) "images": images,
};
final response = await _safeApiCall(
ApiEndpoints.approveReportAction,
method: 'POST',
body: body,
);
if (response == null) return false;
final parsed = _parseAndDecryptResponse(response,
label: "Approve Task", returnFullResponse: true);
return parsed != null && parsed['success'] == true;
}
static Future<bool> createTask({
required String parentTaskId,
required int plannedTask,
required String comment,
required String workAreaId,
required String activityId,
DateTime? assignmentDate,
required String categoryId,
}) async {
final body = [
{
"parentTaskId": parentTaskId,
"plannedWork": plannedTask,
"comment": comment,
"workAreaID": workAreaId,
"activityID": activityId,
"workCategoryId": categoryId,
'completedWork': 0,
}
];
final response = await _safeApiCall(
ApiEndpoints.assignTask,
method: 'POST',
body: body,
);
if (response == null) return false;
final parsed = _parseAndDecryptResponse(response,
label: "Create Task", returnFullResponse: true);
if (parsed != null && parsed['success'] == true) {
Get.back(); // Retaining Get.back() as per original logic
return true;
}
return false;
}
/// ============================================
/// LOGGING & MENU
/// ============================================
static Future<bool> postLogsApi(List<Map<String, dynamic>> 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<Map<String, dynamic>?> 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<String, dynamic> ? jsonResponse : null;
}
/// ============================================
/// ATTENDANCE
/// ============================================
static Future<List<dynamic>?> getProjects() async {
final response =
await _safeApiCall(ApiEndpoints.getProjects, method: 'GET');
if (response == null) return null;
return _parseAndDecryptResponse(response, label: 'Projects');
}
static Future<List<dynamic>?> getGlobalProjects() async {
final response =
await _safeApiCall(ApiEndpoints.getGlobalProjects, method: 'GET');
if (response == null) return null;
return _parseAndDecryptResponse(response, label: 'Global Projects');
}
static Future<List<dynamic>?> 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<List<EmployeeModel>?> 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<String, dynamic>) {
return [EmployeeModel.fromJson(data)];
} else if (data is List) {
return data.map((e) => EmployeeModel.fromJson(e)).toList();
}
return null;
}
static Future<List<dynamic>?> 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<List<dynamic>?> 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<List<dynamic>?> 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<bool> 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<Map<String, dynamic>?> 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<String, dynamic>?;
}
static Future<List<dynamic>?> 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<Map<String, dynamic>?> 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<String, dynamic>?;
}
static Future<Map<String, dynamic>?> 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<String, dynamic>?;
}
static Future<List<String>> 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<String>.from(body);
}
return [];
}
static Future<bool> createContact(Map<String, dynamic> 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<bool> updateContact(
String contactId, Map<String, dynamic> 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<bool> 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<bool> 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<List<dynamic>?> 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<bool> 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<bool> 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<bool> 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<Map<String, dynamic>?> 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<String, dynamic>?;
}
static Future<bool> 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<bool> assignEmployeesToBucket({
required String bucketId,
required List<Map<String, dynamic>> 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<bool> 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<bool> 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;
}
}