- Updated import paths for employee model files to reflect new directory structure. - Deleted obsolete models: JobRecentApplicationModel, LeadReportModel, Product, ProductOrderModal, ProjectSummaryModel, RecentOrderModel, TaskListModel, TimeLineModel, User, VisitorByChannelsModel. - Introduced new AttendanceLogModel, AttendanceLogViewModel, AttendanceModel, TaskModel, TaskListModel, EmployeeInfo, and EmployeeModel with comprehensive fields and JSON serialization methods. - Enhanced data handling in attendance and task management features.
		
			
				
	
	
		
			358 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			358 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| 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<ExpenseModel> expenses = <ExpenseModel>[].obs;
 | |
|   final RxBool isLoading = false.obs;
 | |
|   final RxString errorMessage = ''.obs;
 | |
| 
 | |
|   // Master data
 | |
|   final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
 | |
|   final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
 | |
|   final RxList<ExpenseStatusModel> expenseStatuses = <ExpenseStatusModel>[].obs;
 | |
|   final RxList<String> globalProjects = <String>[].obs;
 | |
|   final RxMap<String, String> projectsMap = <String, String>{}.obs;
 | |
|   RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
 | |
| 
 | |
|   // Persistent Filter States
 | |
|   final RxString selectedProject = ''.obs;
 | |
|   final RxString selectedStatus = ''.obs;
 | |
|   final Rx<DateTime?> startDate = Rx<DateTime?>(null);
 | |
|   final Rx<DateTime?> endDate = Rx<DateTime?>(null);
 | |
|   final RxList<EmployeeModel> selectedPaidByEmployees = <EmployeeModel>[].obs;
 | |
|   final RxList<EmployeeModel> selectedCreatedByEmployees =
 | |
|       <EmployeeModel>[].obs;
 | |
|   final RxString selectedDateType = 'Transaction Date'.obs;
 | |
| 
 | |
|   final employeeSearchController = TextEditingController();
 | |
|   final isSearchingEmployees = false.obs;
 | |
|   final employeeSearchResults = <EmployeeModel>[].obs;
 | |
| 
 | |
|   final List<String> 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<void> loadInitialMasterData() async {
 | |
|     await fetchGlobalProjects();
 | |
|     await fetchMasterData();
 | |
|   }
 | |
| 
 | |
|   Future<void> 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<void> 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<void> fetchExpenses({
 | |
|     List<String>? projectIds,
 | |
|     List<String>? statusIds,
 | |
|     List<String>? createdByIds,
 | |
|     List<String>? 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<String, dynamic> 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<void> 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<void> fetchGlobalProjects() async {
 | |
|     try {
 | |
|       final response = await ApiService.getGlobalProjects();
 | |
|       if (response != null) {
 | |
|         final names = <String>[];
 | |
|         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<void> 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<void> loadMoreExpenses() async {
 | |
|     if (isLoading.value) return;
 | |
| 
 | |
|     _pageNumber += 1;
 | |
|     isLoading.value = true;
 | |
| 
 | |
|     final Map<String, dynamic> 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<bool> 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;
 | |
|     }
 | |
|   }
 | |
| }
 |