import 'package:get/get.dart'; import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:on_field_work/controller/project_controller.dart'; import 'package:on_field_work/model/dashboard/project_progress_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/expense/expense_type_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/model/dashboard/collection_overview_model.dart'; import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart'; class DashboardController extends GetxController { final ProjectController projectController = Get.put(ProjectController()); // -------------------------- // STATE VARIABLES // -------------------------- final roleWiseData = >[].obs; final attendanceSelectedRange = '15D'.obs; final attendanceIsChartView = true.obs; final isAttendanceLoading = false.obs; final projectChartData = [].obs; final projectSelectedRange = '15D'.obs; final projectIsChartView = true.obs; final isProjectLoading = false.obs; final totalProjects = 0.obs; final ongoingProjects = 0.obs; final isProjectsLoading = false.obs; final totalTasks = 0.obs; final completedTasks = 0.obs; final isTasksLoading = false.obs; final totalEmployees = 0.obs; final inToday = 0.obs; final isTeamsLoading = false.obs; final isPendingExpensesLoading = false.obs; final pendingExpensesData = Rx(null); final isExpenseTypeReportLoading = false.obs; final expenseTypeReportData = Rx(null); final expenseReportStartDate = DateTime.now().subtract(const Duration(days: 15)).obs; final expenseReportEndDate = DateTime.now().obs; final isMonthlyExpenseLoading = false.obs; final monthlyExpenseList = [].obs; final selectedMonthlyExpenseDuration = MonthlyExpenseDuration.twelveMonths.obs; final selectedMonthsCount = 12.obs; final expenseTypes = [].obs; final selectedExpenseType = Rx(null); final isLoadingEmployees = true.obs; final employees = [].obs; final uploadingStates = {}.obs; final isCollectionOverviewLoading = true.obs; final collectionOverviewData = Rx(null); final isPurchaseInvoiceLoading = true.obs; final purchaseInvoiceOverviewData = Rx(null); final List ranges = const ['7D', '15D', '30D']; static const _rangeDaysMap = { '7D': 7, '15D': 15, '30D': 30, '3M': 90, '6M': 180 }; static const double _w0_30 = 15.0; static const double _w30_60 = 45.0; static const double _w60_90 = 75.0; static const double _w90_plus = 105.0; // -------------------------- // LATEST PROJECT ID (for race condition fix) // -------------------------- String _latestProjectId = ''; // -------------------------- // COMPUTED PROPERTIES // -------------------------- int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7; int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7; double get calculatedDSO { final data = collectionOverviewData.value; if (data == null || data.totalDueAmount == 0) return 0.0; final double weightedDue = (data.bucket0To30Amount * _w0_30) + (data.bucket30To60Amount * _w30_60) + (data.bucket60To90Amount * _w60_90) + (data.bucket90PlusAmount * _w90_plus); return weightedDue / data.totalDueAmount; } // -------------------------- // LIFECYCLE // -------------------------- @override void onInit() { super.onInit(); logSafe('DashboardController initialized', level: LogLevel.info); // -------------------------- // Project change listener // -------------------------- ever(projectController.selectedProjectId, (id) { if (id.isNotEmpty) { _latestProjectId = id; // track latest project fetchAllDashboardData(id); } }); // Expense Report Date Listener everAll([expenseReportStartDate, expenseReportEndDate], (_) { final id = projectController.selectedProjectId.value; if (id.isNotEmpty) { fetchExpenseTypeReport( startDate: expenseReportStartDate.value, endDate: expenseReportEndDate.value, projectId: id, ); } }); ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance()); ever(projectSelectedRange, (_) => fetchProjectProgress()); } // -------------------------- // USER ACTIONS // -------------------------- void updateAttendanceRange(String range) => attendanceSelectedRange.value = range; void updateProjectRange(String range) => projectSelectedRange.value = range; void toggleAttendanceChartView(bool isChart) => attendanceIsChartView.value = isChart; void toggleProjectChartView(bool isChart) => projectIsChartView.value = isChart; void updateSelectedExpenseType(ExpenseTypeModel? type) { selectedExpenseType.value = type; fetchMonthlyExpenses(categoryId: type?.id); } void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) { selectedMonthlyExpenseDuration.value = duration; const durationMap = { MonthlyExpenseDuration.oneMonth: 1, MonthlyExpenseDuration.threeMonths: 3, MonthlyExpenseDuration.sixMonths: 6, MonthlyExpenseDuration.twelveMonths: 12, MonthlyExpenseDuration.all: 0, }; selectedMonthsCount.value = durationMap[duration] ?? 12; fetchMonthlyExpenses(); } Future refreshDashboard() => fetchAllDashboardData(projectController.selectedProjectId.value); Future refreshAttendance() => fetchRoleWiseAttendance(); Future refreshProjects() => fetchProjectProgress(); Future refreshTasks() async { final id = projectController.selectedProjectId.value; if (id.isNotEmpty) await fetchDashboardTasks(projectId: id); } // -------------------------- // HELPER: Execute API call // -------------------------- Future _executeApiCall( RxBool loaderRx, Future Function() apiLogic) async { loaderRx.value = true; try { await apiLogic(); } catch (e, stack) { logSafe('API Call Failed: $e', level: LogLevel.error, stackTrace: stack); } finally { loaderRx.value = false; } } // -------------------------- // API FETCHES // -------------------------- Future fetchAllDashboardData(String projectId) async { if (projectId.isEmpty) return; _latestProjectId = projectId; await Future.wait([ fetchRoleWiseAttendance(projectId), fetchProjectProgress(projectId), fetchDashboardTasks(projectId: projectId), fetchDashboardTeams(projectId: projectId), fetchPendingExpenses(projectId), fetchExpenseTypeReport( startDate: expenseReportStartDate.value, endDate: expenseReportEndDate.value, projectId: projectId, ), fetchMonthlyExpenses(projectId: projectId), fetchMasterData(), fetchCollectionOverview(projectId), fetchPurchaseInvoiceOverview(projectId), fetchTodaysAttendance(projectId), ]); } // -------------------------- // Each fetch now ignores stale project responses // -------------------------- Future fetchRoleWiseAttendance([String? projectId]) async { final id = projectId ?? projectController.selectedProjectId.value; if (id.isEmpty) return; final localId = id; await _executeApiCall(isAttendanceLoading, () async { final response = await ApiService.getDashboardAttendanceOverview( id, getAttendanceDays()); if (_latestProjectId != localId) return; // discard stale response roleWiseData.assignAll( response?.map((e) => Map.from(e)).toList() ?? []); }); } Future fetchProjectProgress([String? projectId]) async { final id = projectId ?? projectController.selectedProjectId.value; if (id.isEmpty) return; final localId = id; await _executeApiCall(isProjectLoading, () async { final response = await ApiService.getProjectProgress( projectId: id, days: getProjectDays()); if (_latestProjectId != localId) return; if (response?.success == true) { projectChartData.assignAll(response!.data .map((d) => ChartTaskData.fromProjectData(d)) .toList()); } else { projectChartData.clear(); } }); } Future fetchDashboardTasks({required String projectId}) async { final localId = projectId; await _executeApiCall(isTasksLoading, () async { final response = await ApiService.getDashboardTasks(projectId: projectId); if (_latestProjectId != localId) return; totalTasks.value = response?.data?.totalTasks ?? 0; completedTasks.value = response?.data?.completedTasks ?? 0; }); } Future fetchDashboardTeams({required String projectId}) async { final localId = projectId; await _executeApiCall(isTeamsLoading, () async { final response = await ApiService.getDashboardTeams(projectId: projectId); if (_latestProjectId != localId) return; totalEmployees.value = response?.data?.totalEmployees ?? 0; inToday.value = response?.data?.inToday ?? 0; }); } Future fetchPendingExpenses([String? projectId]) async { final id = projectId ?? projectController.selectedProjectId.value; if (id.isEmpty) return; final localId = id; await _executeApiCall(isPendingExpensesLoading, () async { final response = await ApiService.getPendingExpensesApi(projectId: id); if (_latestProjectId != localId) return; pendingExpensesData.value = response?.success == true ? response!.data : null; }); } Future fetchExpenseTypeReport( {required DateTime startDate, required DateTime endDate, String? projectId}) async { final id = projectId ?? projectController.selectedProjectId.value; if (id.isEmpty) return; final localId = id; await _executeApiCall(isExpenseTypeReportLoading, () async { final response = await ApiService.getExpenseTypeReportApi( projectId: id, startDate: startDate, endDate: endDate); if (_latestProjectId != localId) return; expenseTypeReportData.value = response?.success == true ? response!.data : null; }); } Future fetchMonthlyExpenses( {String? categoryId, String? projectId}) async { final id = projectId ?? projectController.selectedProjectId.value; if (id.isEmpty) return; final localId = id; await _executeApiCall(isMonthlyExpenseLoading, () async { final response = await ApiService.getDashboardMonthlyExpensesApi( categoryId: categoryId, months: selectedMonthsCount.value); if (_latestProjectId != localId) return; monthlyExpenseList .assignAll(response?.success == true ? response!.data : []); }); } Future fetchMasterData() async { await _executeApiCall(false.obs, () async { final data = await ApiService.getMasterExpenseTypes(); if (data is List) expenseTypes .assignAll(data.map((e) => ExpenseTypeModel.fromJson(e)).toList()); }); } Future fetchCollectionOverview([String? projectId]) async { final id = projectId ?? projectController.selectedProjectId.value; if (id.isEmpty) return; final localId = id; await _executeApiCall(isCollectionOverviewLoading, () async { final response = await ApiService.getCollectionOverview(projectId: id); if (_latestProjectId != localId) return; collectionOverviewData.value = response?.success == true ? response!.data : null; }); } Future fetchPurchaseInvoiceOverview([String? projectId]) async { final id = projectId ?? projectController.selectedProjectId.value; if (id.isEmpty) return; final localId = id; await _executeApiCall(isPurchaseInvoiceLoading, () async { final response = await ApiService.getPurchaseInvoiceOverview(projectId: id); if (_latestProjectId != localId) return; purchaseInvoiceOverviewData.value = response?.success == true ? response!.data : null; }); } Future fetchTodaysAttendance(String projectId) async { final localId = projectId; await _executeApiCall(isLoadingEmployees, () async { final response = await ApiService.getAttendanceForDashboard(projectId); if (_latestProjectId != localId) return; employees.assignAll(response ?? []); for (var emp in employees) { uploadingStates.putIfAbsent(emp.id, () => false.obs); } }); } } enum MonthlyExpenseDuration { oneMonth, threeMonths, sixMonths, twelveMonths, all, }