import 'dart:convert'; import 'package:get/get.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/model/expense/expense_list_model.dart'; import 'package:marco/model/expense/payment_types_model.dart'; import 'package:marco/model/expense/expense_type_model.dart'; import 'package:marco/model/expense/expense_status_model.dart'; import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:flutter/material.dart'; class ExpenseController extends GetxController { final RxList expenses = [].obs; final RxBool isLoading = false.obs; final RxString errorMessage = ''.obs; // Master data final RxList expenseTypes = [].obs; final RxList paymentModes = [].obs; final RxList expenseStatuses = [].obs; final RxList globalProjects = [].obs; final RxMap projectsMap = {}.obs; RxList allEmployees = [].obs; // Persistent Filter States final RxString selectedProject = ''.obs; final RxString selectedStatus = ''.obs; final Rx startDate = Rx(null); final Rx endDate = Rx(null); final RxList selectedPaidByEmployees = [].obs; final RxList selectedCreatedByEmployees = [].obs; final RxString selectedDateType = 'Transaction Date'.obs; final employeeSearchController = TextEditingController(); final isSearchingEmployees = false.obs; final employeeSearchResults = [].obs; final List dateTypes = [ 'Transaction Date', 'Created At', ]; int _pageSize = 20; int _pageNumber = 1; @override void onInit() { super.onInit(); loadInitialMasterData(); fetchAllEmployees(); employeeSearchController.addListener(() { searchEmployees(employeeSearchController.text); }); } bool get isFilterApplied { return selectedProject.value.isNotEmpty || selectedStatus.value.isNotEmpty || startDate.value != null || endDate.value != null || selectedPaidByEmployees.isNotEmpty || selectedCreatedByEmployees.isNotEmpty; } /// Load master data Future loadInitialMasterData() async { await fetchGlobalProjects(); await fetchMasterData(); } Future deleteExpense(String expenseId) async { try { logSafe("Attempting to delete expense: $expenseId"); final success = await ApiService.deleteExpense(expenseId); if (success) { expenses.removeWhere((e) => e.id == expenseId); logSafe("Expense deleted successfully."); showAppSnackbar( title: "Deleted", message: "Expense has been deleted successfully.", type: SnackbarType.success, ); } else { logSafe("Failed to delete expense: $expenseId", level: LogLevel.error); showAppSnackbar( title: "Failed", message: "Failed to delete expense.", type: SnackbarType.error, ); } } catch (e, stack) { logSafe("Exception in deleteExpense: $e", level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); showAppSnackbar( title: "Error", message: "Something went wrong while deleting.", type: SnackbarType.error, ); } } Future searchEmployees(String searchQuery) async { if (searchQuery.trim().isEmpty) { employeeSearchResults.clear(); return; } isSearchingEmployees.value = true; try { final results = await ApiService.searchEmployeesBasic( searchString: searchQuery.trim(), ); if (results != null) { employeeSearchResults.assignAll( results.map((e) => EmployeeModel.fromJson(e)), ); } else { employeeSearchResults.clear(); } } catch (e) { logSafe("Error searching employees: $e", level: LogLevel.error); employeeSearchResults.clear(); } finally { isSearchingEmployees.value = false; } } /// Fetch expenses using filters Future fetchExpenses({ List? projectIds, List? statusIds, List? createdByIds, List? paidByIds, DateTime? startDate, DateTime? endDate, int pageSize = 20, int pageNumber = 1, }) async { isLoading.value = true; errorMessage.value = ''; expenses.clear(); _pageSize = pageSize; _pageNumber = pageNumber; final Map filterMap = { "projectIds": projectIds ?? (selectedProject.value.isEmpty ? [] : [projectsMap[selectedProject.value] ?? '']), "statusIds": statusIds ?? (selectedStatus.value.isEmpty ? [] : [selectedStatus.value]), "createdByIds": createdByIds ?? selectedCreatedByEmployees.map((e) => e.id).toList(), "paidByIds": paidByIds ?? selectedPaidByEmployees.map((e) => e.id).toList(), "startDate": (startDate ?? this.startDate.value)?.toIso8601String(), "endDate": (endDate ?? this.endDate.value)?.toIso8601String(), "isTransactionDate": selectedDateType.value == 'Transaction Date', }; try { logSafe("Fetching expenses with filter: ${jsonEncode(filterMap)}"); final result = await ApiService.getExpenseListApi( filter: jsonEncode(filterMap), pageSize: _pageSize, pageNumber: _pageNumber, ); if (result != null) { try { final expenseResponse = ExpenseResponse.fromJson(result); // If the backend returns no data, treat it as empty list if (expenseResponse.data.data.isEmpty) { expenses.clear(); errorMessage.value = ''; // no error logSafe("Expense list is empty."); } else { expenses.assignAll(expenseResponse.data.data); logSafe("Expenses loaded: ${expenses.length}"); logSafe( "Pagination Info: Page ${expenseResponse.data.currentPage} of ${expenseResponse.data.totalPages} | Total: ${expenseResponse.data.totalEntites}"); } } catch (e) { errorMessage.value = 'Failed to parse expenses: $e'; logSafe("Parse error in fetchExpenses: $e", level: LogLevel.error); } } else { // Only treat as error if this means a network or server failure errorMessage.value = 'Unable to connect to the server.'; logSafe("fetchExpenses failed: null response", level: LogLevel.error); } } catch (e, stack) { errorMessage.value = 'An unexpected error occurred.'; logSafe("Exception in fetchExpenses: $e", level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); } finally { isLoading.value = false; } } /// Clear all filters void clearFilters() { selectedProject.value = ''; selectedStatus.value = ''; startDate.value = null; endDate.value = null; selectedPaidByEmployees.clear(); selectedCreatedByEmployees.clear(); } /// Fetch master data: expense types, payment modes, and expense status Future fetchMasterData() async { try { final expenseTypesData = await ApiService.getMasterExpenseTypes(); if (expenseTypesData is List) { expenseTypes.value = expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList(); } final paymentModesData = await ApiService.getMasterPaymentModes(); if (paymentModesData is List) { paymentModes.value = paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList(); } final expenseStatusData = await ApiService.getMasterExpenseStatus(); if (expenseStatusData is List) { expenseStatuses.value = expenseStatusData .map((e) => ExpenseStatusModel.fromJson(e)) .toList(); } } catch (e) { showAppSnackbar( title: "Error", message: "Failed to fetch master data: $e", type: SnackbarType.error, ); } } /// Fetch global projects Future fetchGlobalProjects() async { try { final response = await ApiService.getGlobalProjects(); if (response != null) { final names = []; for (var item in response) { final name = item['name']?.toString().trim(); final id = item['id']?.toString().trim(); if (name != null && id != null && name.isNotEmpty) { projectsMap[name] = id; names.add(name); } } globalProjects.assignAll(names); logSafe("Fetched ${names.length} global projects"); } } catch (e) { logSafe("Failed to fetch global projects: $e", level: LogLevel.error); } } /// Fetch all employees Future fetchAllEmployees() async { isLoading.value = true; try { final response = await ApiService.getAllEmployees(); if (response != null && response.isNotEmpty) { allEmployees .assignAll(response.map((json) => EmployeeModel.fromJson(json))); logSafe( "All Employees fetched for Manage Bucket: ${allEmployees.length}", level: LogLevel.info, ); } else { allEmployees.clear(); logSafe("No employees found for Manage Bucket.", level: LogLevel.warning); } } catch (e) { allEmployees.clear(); logSafe("Error fetching employees in Manage Bucket", level: LogLevel.error, error: e); } isLoading.value = false; update(); } Future loadMoreExpenses() async { if (isLoading.value) return; _pageNumber += 1; isLoading.value = true; final Map filterMap = { "projectIds": selectedProject.value.isEmpty ? [] : [projectsMap[selectedProject.value] ?? ''], "statusIds": selectedStatus.value.isEmpty ? [] : [selectedStatus.value], "createdByIds": selectedCreatedByEmployees.map((e) => e.id).toList(), "paidByIds": selectedPaidByEmployees.map((e) => e.id).toList(), "startDate": startDate.value?.toIso8601String(), "endDate": endDate.value?.toIso8601String(), "isTransactionDate": selectedDateType.value == 'Transaction Date', }; try { final result = await ApiService.getExpenseListApi( filter: jsonEncode(filterMap), pageSize: _pageSize, pageNumber: _pageNumber, ); if (result != null) { final expenseResponse = ExpenseResponse.fromJson(result); expenses.addAll(expenseResponse.data.data); } } catch (e) { logSafe("Error in loadMoreExpenses: $e", level: LogLevel.error); } finally { isLoading.value = false; } } /// Update expense status Future updateExpenseStatus(String expenseId, String statusId) async { isLoading.value = true; errorMessage.value = ''; try { logSafe("Updating status for expense: $expenseId -> $statusId"); final success = await ApiService.updateExpenseStatusApi( expenseId: expenseId, statusId: statusId, ); if (success) { logSafe("Expense status updated successfully."); await fetchExpenses(); return true; } else { errorMessage.value = "Failed to update expense status."; return false; } } catch (e, stack) { errorMessage.value = 'An unexpected error occurred.'; logSafe("Exception in updateExpenseStatus: $e", level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); return false; } finally { isLoading.value = false; } } }