- 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.
188 lines
6.4 KiB
Dart
188 lines
6.4 KiB
Dart
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_detail_model.dart';
|
|
import 'package:marco/model/employees/employee_model.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
class ExpenseDetailController extends GetxController {
|
|
final Rx<ExpenseDetailModel?> expense = Rx<ExpenseDetailModel?>(null);
|
|
final RxBool isLoading = false.obs;
|
|
final RxString errorMessage = ''.obs;
|
|
final Rx<EmployeeModel?> selectedReimbursedBy = Rx<EmployeeModel?>(null);
|
|
final RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
|
final RxList<EmployeeModel> employeeSearchResults = <EmployeeModel>[].obs;
|
|
late String _expenseId;
|
|
bool _isInitialized = false;
|
|
final employeeSearchController = TextEditingController();
|
|
final isSearchingEmployees = false.obs;
|
|
|
|
/// Call this once from the screen (NOT inside build) to initialize
|
|
void init(String expenseId) {
|
|
if (_isInitialized) return;
|
|
|
|
_isInitialized = true;
|
|
_expenseId = expenseId;
|
|
|
|
// Use Future.wait to fetch details and employees concurrently
|
|
Future.wait([
|
|
fetchExpenseDetails(),
|
|
fetchAllEmployees(),
|
|
]);
|
|
}
|
|
|
|
/// Generic method to handle API calls with loading and error states
|
|
Future<T?> _apiCallWrapper<T>(
|
|
Future<T?> Function() apiCall, String operationName) async {
|
|
isLoading.value = true;
|
|
errorMessage.value = ''; // Clear previous errors
|
|
|
|
try {
|
|
logSafe("Initiating $operationName...");
|
|
final result = await apiCall();
|
|
logSafe("$operationName completed successfully.");
|
|
return result;
|
|
} catch (e, stack) {
|
|
errorMessage.value =
|
|
'An unexpected error occurred during $operationName.';
|
|
logSafe("Exception in $operationName: $e", level: LogLevel.error);
|
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
|
return null;
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
/// Fetch expense details by stored ID
|
|
Future<void> fetchExpenseDetails() async {
|
|
final result = await _apiCallWrapper(
|
|
() => ApiService.getExpenseDetailsApi(expenseId: _expenseId),
|
|
"fetch expense details");
|
|
|
|
if (result != null) {
|
|
try {
|
|
expense.value = ExpenseDetailModel.fromJson(result);
|
|
logSafe("Expense details loaded successfully: ${expense.value?.id}");
|
|
} catch (e) {
|
|
errorMessage.value = 'Failed to parse expense details: $e';
|
|
logSafe("Parse error in fetchExpenseDetails: $e",
|
|
level: LogLevel.error);
|
|
}
|
|
} else {
|
|
errorMessage.value = 'Failed to fetch expense details from server.';
|
|
logSafe("fetchExpenseDetails failed: null response",
|
|
level: LogLevel.error);
|
|
}
|
|
}
|
|
|
|
// This method seems like a utility and might be better placed in a helper or utility class
|
|
// if it's used across multiple controllers. Keeping it here for now as per original code.
|
|
List<String> parsePermissionIds(dynamic permissionData) {
|
|
if (permissionData == null) return [];
|
|
if (permissionData is List) {
|
|
return permissionData
|
|
.map((e) => e.toString().trim())
|
|
.where((e) => e.isNotEmpty)
|
|
.toList();
|
|
}
|
|
if (permissionData is String) {
|
|
final clean = permissionData.replaceAll(RegExp(r'[\[\]]'), '');
|
|
return clean
|
|
.split(',')
|
|
.map((e) => e.trim())
|
|
.where((e) => e.isNotEmpty)
|
|
.toList();
|
|
}
|
|
return [];
|
|
}
|
|
|
|
Future<void> searchEmployees(String query) async {
|
|
if (query.trim().isEmpty) return employeeSearchResults.clear();
|
|
isSearchingEmployees.value = true;
|
|
try {
|
|
final data =
|
|
await ApiService.searchEmployeesBasic(searchString: query.trim());
|
|
employeeSearchResults.assignAll(
|
|
(data ?? []).map((e) => EmployeeModel.fromJson(e)),
|
|
);
|
|
} catch (e) {
|
|
logSafe("Error searching employees: $e", level: LogLevel.error);
|
|
employeeSearchResults.clear();
|
|
} finally {
|
|
isSearchingEmployees.value = false;
|
|
}
|
|
}
|
|
|
|
/// Fetch all employees
|
|
Future<void> fetchAllEmployees() async {
|
|
final response = await _apiCallWrapper(
|
|
() => ApiService.getAllEmployees(), "fetch all employees");
|
|
|
|
if (response != null && response.isNotEmpty) {
|
|
try {
|
|
allEmployees.assignAll(response.map((e) => EmployeeModel.fromJson(e)));
|
|
logSafe("All Employees fetched: ${allEmployees.length}",
|
|
level: LogLevel.info);
|
|
} catch (e) {
|
|
errorMessage.value = 'Failed to parse employee data: $e';
|
|
logSafe("Parse error in fetchAllEmployees: $e", level: LogLevel.error);
|
|
}
|
|
} else {
|
|
allEmployees.clear();
|
|
logSafe("No employees found.", level: LogLevel.warning);
|
|
}
|
|
// `update()` is typically not needed for RxList directly unless you have specific GetBuilder/Obx usage that requires it
|
|
// If you are using Obx widgets, `allEmployees.assignAll` will automatically trigger a rebuild.
|
|
}
|
|
|
|
/// Update expense with reimbursement info and status
|
|
Future<bool> updateExpenseStatusWithReimbursement({
|
|
required String comment,
|
|
required String reimburseTransactionId,
|
|
required String reimburseDate,
|
|
required String reimburseById,
|
|
required String statusId,
|
|
}) async {
|
|
final success = await _apiCallWrapper(
|
|
() => ApiService.updateExpenseStatusApi(
|
|
expenseId: _expenseId,
|
|
statusId: statusId,
|
|
comment: comment,
|
|
reimburseTransactionId: reimburseTransactionId,
|
|
reimburseDate: reimburseDate,
|
|
reimbursedById: reimburseById,
|
|
),
|
|
"submit reimbursement",
|
|
);
|
|
|
|
if (success == true) {
|
|
// Explicitly check for true as _apiCallWrapper returns T?
|
|
await fetchExpenseDetails(); // Refresh details after successful update
|
|
return true;
|
|
} else {
|
|
errorMessage.value = "Failed to submit reimbursement.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Update status for this specific expense
|
|
Future<bool> updateExpenseStatus(String statusId, {String? comment}) async {
|
|
final success = await _apiCallWrapper(
|
|
() => ApiService.updateExpenseStatusApi(
|
|
expenseId: _expenseId,
|
|
statusId: statusId,
|
|
comment: comment,
|
|
),
|
|
"update expense status",
|
|
);
|
|
|
|
if (success == true) {
|
|
await fetchExpenseDetails();
|
|
return true;
|
|
} else {
|
|
errorMessage.value = "Failed to update expense status.";
|
|
return false;
|
|
}
|
|
}
|
|
}
|