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 // ========================= // 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); 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 = ['7D', '15D', '30D']; static const _rangeDaysMap = { '7D': 7, '15D': 15, '30D': 30, '3M': 90, '6M': 180 }; // ========================= // 2. COMPUTED PROPERTIES // ========================= int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7; int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7; // DSO Calculation Constants 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; 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 // ========================= @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 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 // ========================= 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; // Efficient Map lookup instead of Switch 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 Future _executeApiCall( RxBool loader, Future Function() apiLogic) async { loader.value = true; try { await apiLogic(); } finally { loader.value = false; } } Future fetchAllDashboardData() async { final String projectId = projectController.selectedProjectId.value; if (projectId.isEmpty) return; 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); 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; for (var emp in employees) { uploadingStates.putIfAbsent(emp.id, () => false.obs); } } }); } Future fetchMasterData() async { try { final data = await ApiService.getMasterExpenseTypes(); if (data is List) { expenseTypes.value = data.map((e) => ExpenseTypeModel.fromJson(e)).toList(); } } catch (_) {} } Future fetchMonthlyExpenses({String? categoryId}) async { await _executeApiCall(isMonthlyExpenseLoading, () async { final response = await ApiService.getDashboardMonthlyExpensesApi( categoryId: categoryId, months: selectedMonthsCount.value, ); 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, ); 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); 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()); 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, ); 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(); } }); } Future fetchDashboardTasks({required String projectId}) async { await _executeApiCall(isTasksLoading, () async { final response = await ApiService.getDashboardTasks(projectId: projectId); if (response?.success == true) { 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) { 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, }