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 { // Dependencies final ProjectController projectController = Get.put(ProjectController()); // ========================= // 1. STATE VARIABLES (No functional change) // ========================= // Attendance final roleWiseData = >[].obs; final attendanceSelectedRange = '15D'.obs; final attendanceIsChartView = true.obs; final isAttendanceLoading = false.obs; // Project Progress final projectChartData = [].obs; final projectSelectedRange = '15D'.obs; final projectIsChartView = true.obs; final isProjectLoading = false.obs; // Overview Counts 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; // Expenses & Reports final isPendingExpensesLoading = false.obs; final pendingExpensesData = Rx(null); final isExpenseTypeReportLoading = false.obs; final expenseTypeReportData = Rx(null); // OPTIMIZED: Use const Duration for better performance 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); // Teams/Employees final isLoadingEmployees = true.obs; final employees = [].obs; final uploadingStates = {}.obs; // Collection final isCollectionOverviewLoading = true.obs; final collectionOverviewData = Rx(null); // ========================= // Purchase Invoice Overview // ========================= final isPurchaseInvoiceLoading = true.obs; final purchaseInvoiceOverviewData = Rx(null); // Constants final List ranges = const [ '7D', '15D', '30D' ]; // OPTIMIZED: Added const static const _rangeDaysMap = { // OPTIMIZED: Added const '7D': 7, '15D': 15, '30D': 30, '3M': 90, '6M': 180 }; // DSO Calculation Constants (OPTIMIZED: Added const) 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; // ========================= // 2. COMPUTED PROPERTIES (No functional change) // ========================= 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; } // ========================= // 3. LIFECYCLE (No functional change) // ========================= @override void onInit() { super.onInit(); logSafe('DashboardController initialized', level: LogLevel.info); // Project Selection Listener ever(projectController.selectedProjectId, (id) { if (id.isNotEmpty) { fetchAllDashboardData(); fetchTodaysAttendance(id); } }); // Expense Report Date Listener // OPTIMIZED: Using `everAll` is already efficient for this logic everAll([expenseReportStartDate, expenseReportEndDate], (_) { if (projectController.selectedProjectId.value.isNotEmpty) { fetchExpenseTypeReport( startDate: expenseReportStartDate.value, endDate: expenseReportEndDate.value, ); } }); // Chart Range Listeners ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance()); ever(projectSelectedRange, (_) => fetchProjectProgress()); } // ========================= // 4. USER ACTIONS (No functional change) // ========================= 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; // OPTIMIZED: The map approach is highly efficient. 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(); Future refreshAttendance() => fetchRoleWiseAttendance(); Future refreshProjects() => fetchProjectProgress(); Future refreshTasks() async { final id = projectController.selectedProjectId.value; if (id.isNotEmpty) await fetchDashboardTasks(projectId: id); } // ========================= // 5. DATA FETCHING (API) // ========================= /// Wrapper to reduce try-finally boilerplate for loading states // OPTIMIZED: Renamed variable to avoid shadowing standard library. Future _executeApiCall( RxBool loaderRx, Future Function() apiLogic) async { loaderRx.value = true; try { await apiLogic(); } catch (e, stack) { // OPTIMIZED: Added logging of error for better debugging logSafe('API Call Failed: $e', level: LogLevel.error, stackTrace: stack); } finally { loaderRx.value = false; } } Future fetchAllDashboardData() async { final String projectId = projectController.selectedProjectId.value; if (projectId.isEmpty) return; // OPTIMIZED: Ensure MasterData is fetched only once if possible, but kept in Future.wait for robustness await Future.wait([ fetchRoleWiseAttendance(), fetchProjectProgress(), fetchDashboardTasks(projectId: projectId), fetchDashboardTeams(projectId: projectId), fetchPendingExpenses(), fetchExpenseTypeReport( startDate: expenseReportStartDate.value, endDate: expenseReportEndDate.value, ), fetchMonthlyExpenses(), fetchMasterData(), fetchCollectionOverview(), fetchPurchaseInvoiceOverview(), ]); } Future fetchCollectionOverview() async { final projectId = projectController.selectedProjectId.value; if (projectId.isEmpty) return; await _executeApiCall(isCollectionOverviewLoading, () async { final response = await ApiService.getCollectionOverview(projectId: projectId); // OPTIMIZED: Used null-aware assignment collectionOverviewData.value = (response?.success == true) ? response!.data : null; }); } Future fetchTodaysAttendance(String projectId) async { await _executeApiCall(isLoadingEmployees, () async { final response = await ApiService.getAttendanceForDashboard(projectId); if (response != null) { employees.value = response; // OPTIMIZED: Use `putIfAbsent` and ensure the map holds an RxBool for (var emp in employees) { uploadingStates.putIfAbsent(emp.id, () => false.obs); } } else { employees.clear(); } }); } Future fetchMasterData() async { // OPTIMIZATION: Use _executeApiCall for consistency await _executeApiCall(false.obs, () async { // Use a local RxBool since there's no dedicated loader state final data = await ApiService.getMasterExpenseTypes(); if (data is List) { expenseTypes.value = data.map((e) => ExpenseTypeModel.fromJson(e)).toList(); } }); } Future fetchMonthlyExpenses({String? categoryId}) async { await _executeApiCall(isMonthlyExpenseLoading, () async { final response = await ApiService.getDashboardMonthlyExpensesApi( categoryId: categoryId, months: selectedMonthsCount.value, ); // OPTIMIZED: Used null-aware assignment monthlyExpenseList.value = (response?.success == true) ? response!.data : []; }); } Future fetchPurchaseInvoiceOverview() async { final projectId = projectController.selectedProjectId.value; if (projectId.isEmpty) return; await _executeApiCall(isPurchaseInvoiceLoading, () async { final response = await ApiService.getPurchaseInvoiceOverview( projectId: projectId, ); // OPTIMIZED: Used null-aware assignment purchaseInvoiceOverviewData.value = (response?.success == true) ? response!.data : null; }); } Future fetchPendingExpenses() async { final id = projectController.selectedProjectId.value; if (id.isEmpty) return; await _executeApiCall(isPendingExpensesLoading, () async { final response = await ApiService.getPendingExpensesApi(projectId: id); // OPTIMIZED: Used null-aware assignment pendingExpensesData.value = (response?.success == true) ? response!.data : null; }); } Future fetchRoleWiseAttendance() async { final id = projectController.selectedProjectId.value; if (id.isEmpty) return; await _executeApiCall(isAttendanceLoading, () async { final response = await ApiService.getDashboardAttendanceOverview( id, getAttendanceDays()); // OPTIMIZED: Used null-aware assignment roleWiseData.value = response?.map((e) => Map.from(e)).toList() ?? []; }); } Future fetchExpenseTypeReport( {required DateTime startDate, required DateTime endDate}) async { final id = projectController.selectedProjectId.value; if (id.isEmpty) return; await _executeApiCall(isExpenseTypeReportLoading, () async { final response = await ApiService.getExpenseTypeReportApi( projectId: id, startDate: startDate, endDate: endDate, ); // OPTIMIZED: Used null-aware assignment expenseTypeReportData.value = (response?.success == true) ? response!.data : null; }); } Future fetchProjectProgress() async { final id = projectController.selectedProjectId.value; if (id.isEmpty) return; await _executeApiCall(isProjectLoading, () async { final response = await ApiService.getProjectProgress( projectId: id, days: getProjectDays()); if (response?.success == true) { projectChartData.value = response!.data .map((d) => ChartTaskData.fromProjectData(d)) .toList(); } else { projectChartData.clear(); // OPTIMIZED: Clear data on failure } }); } Future fetchDashboardTasks({required String projectId}) async { await _executeApiCall(isTasksLoading, () async { final response = await ApiService.getDashboardTasks(projectId: projectId); if (response?.success == true) { // OPTIMIZED: Used null-aware access with default value totalTasks.value = response!.data?.totalTasks ?? 0; completedTasks.value = response.data?.completedTasks ?? 0; } else { totalTasks.value = 0; completedTasks.value = 0; } }); } Future fetchDashboardTeams({required String projectId}) async { await _executeApiCall(isTeamsLoading, () async { final response = await ApiService.getDashboardTeams(projectId: projectId); if (response?.success == true) { // OPTIMIZED: Used null-aware access with default value totalEmployees.value = response!.data?.totalEmployees ?? 0; inToday.value = response.data?.inToday ?? 0; } else { totalEmployees.value = 0; inToday.value = 0; } }); } } enum MonthlyExpenseDuration { oneMonth, threeMonths, sixMonths, twelveMonths, all, }