- 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;
|
|
}
|
|
}
|
|
}
|