diff --git a/lib/controller/dashboard/dashboard_controller.dart b/lib/controller/dashboard/dashboard_controller.dart index e2a5cf1..3681b4c 100644 --- a/lib/controller/dashboard/dashboard_controller.dart +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -11,134 +11,111 @@ import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/model/dashboard/collection_overview_model.dart'; class DashboardController extends GetxController { - // ========================= - // Attendance overview - // ========================= - final RxList> roleWiseData = - >[].obs; - final RxString attendanceSelectedRange = '15D'.obs; - final RxBool attendanceIsChartView = true.obs; - final RxBool isAttendanceLoading = false.obs; - - // ========================= - // Project progress overview - // ========================= - final RxList projectChartData = [].obs; - final RxString projectSelectedRange = '15D'.obs; - final RxBool projectIsChartView = true.obs; - final RxBool isProjectLoading = false.obs; - - // ========================= - // Projects overview - // ========================= - final RxInt totalProjects = 0.obs; - final RxInt ongoingProjects = 0.obs; - final RxBool isProjectsLoading = false.obs; - - // ========================= - // Tasks overview - // ========================= - final RxInt totalTasks = 0.obs; - final RxInt completedTasks = 0.obs; - final RxBool isTasksLoading = false.obs; - - // ========================= - // Teams overview - // ========================= - final RxInt totalEmployees = 0.obs; - final RxInt inToday = 0.obs; - final RxBool isTeamsLoading = false.obs; - - // Common ranges - final List ranges = ['7D', '15D', '30D']; - - // Inject ProjectController + // Dependencies final ProjectController projectController = Get.put(ProjectController()); // ========================= - // Pending Expenses overview + // 1. STATE VARIABLES // ========================= - final RxBool isPendingExpensesLoading = false.obs; - final Rx pendingExpensesData = - Rx(null); - // ========================= - // Expense Category Report - // ========================= - final RxBool isExpenseTypeReportLoading = false.obs; - final Rx expenseTypeReportData = - Rx(null); - final Rx expenseReportStartDate = + // 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 Rx expenseReportEndDate = DateTime.now().obs; + final expenseReportEndDate = DateTime.now().obs; - // ========================= - // Monthly Expense Report - // ========================= - final RxBool isMonthlyExpenseLoading = false.obs; - final RxList monthlyExpenseList = - [].obs; - - // Filters - final Rx selectedMonthlyExpenseDuration = + final isMonthlyExpenseLoading = false.obs; + final monthlyExpenseList = [].obs; + final selectedMonthlyExpenseDuration = MonthlyExpenseDuration.twelveMonths.obs; - final RxInt selectedMonthsCount = 12.obs; + final selectedMonthsCount = 12.obs; - final RxList expenseTypes = [].obs; - final Rx selectedExpenseType = Rx(null); + final expenseTypes = [].obs; + final selectedExpenseType = Rx(null); + // Teams/Employees final isLoadingEmployees = true.obs; - final RxList employees = [].obs; + final employees = [].obs; final uploadingStates = {}.obs; - // ========================= - // Collection Overview - // ========================= - final RxBool isCollectionOverviewLoading = false.obs; - final Rx collectionOverviewData = - Rx(null); + // Collection + final isCollectionOverviewLoading = false.obs; + final collectionOverviewData = 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; - // ============================================================ - // ⭐ NEW — DSO CALCULATION (Weighted Aging Method) - // ============================================================ double get calculatedDSO { final data = collectionOverviewData.value; if (data == null || data.totalDueAmount == 0) return 0.0; - final double totalDue = data.totalDueAmount; + final double weightedDue = (data.bucket0To30Amount * _w0_30) + + (data.bucket30To60Amount * _w30_60) + + (data.bucket60To90Amount * _w60_90) + + (data.bucket90PlusAmount * _w90_plus); - // Weighted aging midpoints - const d0_30 = 15.0; - const d30_60 = 45.0; - const d60_90 = 75.0; - const d90_plus = 105.0; // conservative estimate - - final double weightedDue = (data.bucket0To30Amount * d0_30) + - (data.bucket30To60Amount * d30_60) + - (data.bucket60To90Amount * d60_90) + - (data.bucket90PlusAmount * d90_plus); - - return weightedDue / totalDue; // Final DSO + return weightedDue / data.totalDueAmount; } - // Update selected expense type - void updateSelectedExpenseType(ExpenseTypeModel? type) { - selectedExpenseType.value = type; - - if (type == null) { - fetchMonthlyExpenses(); - } else { - fetchMonthlyExpenses(categoryId: type.id); - } - } + // ========================= + // 3. LIFECYCLE + // ========================= @override void onInit() { super.onInit(); - logSafe('DashboardController initialized', level: LogLevel.info); - // React to project selection + // Project Selection Listener ever(projectController.selectedProjectId, (id) { if (id.isNotEmpty) { fetchAllDashboardData(); @@ -146,7 +123,7 @@ class DashboardController extends GetxController { } }); - // React to date range changes in expense report + // Expense Report Date Listener everAll([expenseReportStartDate, expenseReportEndDate], (_) { if (projectController.selectedProjectId.value.isNotEmpty) { fetchExpenseTypeReport( @@ -156,62 +133,67 @@ class DashboardController extends GetxController { } }); - // Attendance range + // Chart Range Listeners ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance()); - - // Project range ever(projectSelectedRange, (_) => fetchProjectProgress()); } // ========================= - // Helper Methods + // 4. USER ACTIONS // ========================= - int _getDaysFromRange(String range) { - switch (range) { - case '7D': - return 7; - case '15D': - return 15; - case '30D': - return 30; - case '3M': - return 90; - case '6M': - return 180; - default: - return 7; - } - } - - int getAttendanceDays() => _getDaysFromRange(attendanceSelectedRange.value); - int getProjectDays() => _getDaysFromRange(projectSelectedRange.value); 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; - // ========================= - // Manual Refresh - // ========================= - Future refreshDashboard() async => fetchAllDashboardData(); - Future refreshAttendance() async => fetchRoleWiseAttendance(); + 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); } - Future refreshProjects() async => fetchProjectProgress(); + // ========================= + // 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; + } + } - // ========================= - // Fetch All Dashboard Data - // ========================= Future fetchAllDashboardData() async { final String projectId = projectController.selectedProjectId.value; if (projectId.isEmpty) return; @@ -232,70 +214,28 @@ class DashboardController extends GetxController { ]); } - // ========================= - // API Calls - // ========================= - Future fetchCollectionOverview() async { final projectId = projectController.selectedProjectId.value; if (projectId.isEmpty) return; - try { - isCollectionOverviewLoading.value = true; - + await _executeApiCall(isCollectionOverviewLoading, () async { final response = await ApiService.getCollectionOverview(projectId: projectId); - - if (response != null && response.success) { - collectionOverviewData.value = response.data; - } else { - collectionOverviewData.value = null; - } - } finally { - isCollectionOverviewLoading.value = false; - } + collectionOverviewData.value = + (response?.success == true) ? response!.data : null; + }); } Future fetchTodaysAttendance(String projectId) async { - isLoadingEmployees.value = true; - - final response = await ApiService.getAttendanceForDashboard(projectId); - - if (response != null) { - employees.value = response; - for (var emp in employees) { - uploadingStates[emp.id] = false.obs; + 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); + } } - } - - isLoadingEmployees.value = false; - update(); - } - - void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) { - selectedMonthlyExpenseDuration.value = duration; - - // Set months count based on selection - switch (duration) { - case MonthlyExpenseDuration.oneMonth: - selectedMonthsCount.value = 1; - break; - case MonthlyExpenseDuration.threeMonths: - selectedMonthsCount.value = 3; - break; - case MonthlyExpenseDuration.sixMonths: - selectedMonthsCount.value = 6; - break; - case MonthlyExpenseDuration.twelveMonths: - selectedMonthsCount.value = 12; - break; - case MonthlyExpenseDuration.all: - selectedMonthsCount.value = 0; // 0 = All months in your API - break; - } - - // Re-fetch updated data - fetchMonthlyExpenses(); + }); } Future fetchMasterData() async { @@ -309,149 +249,96 @@ class DashboardController extends GetxController { } Future fetchMonthlyExpenses({String? categoryId}) async { - try { - isMonthlyExpenseLoading.value = true; - + await _executeApiCall(isMonthlyExpenseLoading, () async { final response = await ApiService.getDashboardMonthlyExpensesApi( categoryId: categoryId, months: selectedMonthsCount.value, ); - - if (response != null && response.success) { - monthlyExpenseList.value = response.data; - } else { - monthlyExpenseList.clear(); - } - } finally { - isMonthlyExpenseLoading.value = false; - } + monthlyExpenseList.value = + (response?.success == true) ? response!.data : []; + }); } Future fetchPendingExpenses() async { final id = projectController.selectedProjectId.value; if (id.isEmpty) return; - try { - isPendingExpensesLoading.value = true; - + await _executeApiCall(isPendingExpensesLoading, () async { final response = await ApiService.getPendingExpensesApi(projectId: id); - - if (response != null && response.success) { - pendingExpensesData.value = response.data; - } else { - pendingExpensesData.value = null; - } - } finally { - isPendingExpensesLoading.value = false; - } + pendingExpensesData.value = + (response?.success == true) ? response!.data : null; + }); } Future fetchRoleWiseAttendance() async { final id = projectController.selectedProjectId.value; if (id.isEmpty) return; - try { - isAttendanceLoading.value = true; - + await _executeApiCall(isAttendanceLoading, () async { final response = await ApiService.getDashboardAttendanceOverview( - id, - getAttendanceDays(), - ); - - if (response != null) { - roleWiseData.value = - response.map((e) => Map.from(e)).toList(); - } else { - roleWiseData.clear(); - } - } finally { - isAttendanceLoading.value = false; - } + id, getAttendanceDays()); + roleWiseData.value = + response?.map((e) => Map.from(e)).toList() ?? []; + }); } - Future fetchExpenseTypeReport({ - required DateTime startDate, - required DateTime endDate, - }) async { + Future fetchExpenseTypeReport( + {required DateTime startDate, required DateTime endDate}) async { final id = projectController.selectedProjectId.value; if (id.isEmpty) return; - try { - isExpenseTypeReportLoading.value = true; - + await _executeApiCall(isExpenseTypeReportLoading, () async { final response = await ApiService.getExpenseTypeReportApi( projectId: id, startDate: startDate, endDate: endDate, ); - - if (response != null && response.success) { - expenseTypeReportData.value = response.data; - } else { - expenseTypeReportData.value = null; - } - } finally { - isExpenseTypeReportLoading.value = false; - } + expenseTypeReportData.value = + (response?.success == true) ? response!.data : null; + }); } Future fetchProjectProgress() async { final id = projectController.selectedProjectId.value; if (id.isEmpty) return; - try { - isProjectLoading.value = true; - + await _executeApiCall(isProjectLoading, () async { final response = await ApiService.getProjectProgress( - projectId: id, - days: getProjectDays(), - ); - - if (response != null && response.success) { - projectChartData.value = - response.data.map((d) => ChartTaskData.fromProjectData(d)).toList(); + projectId: id, days: getProjectDays()); + if (response?.success == true) { + projectChartData.value = response!.data + .map((d) => ChartTaskData.fromProjectData(d)) + .toList(); } else { projectChartData.clear(); } - } finally { - isProjectLoading.value = false; - } + }); } Future fetchDashboardTasks({required String projectId}) async { - try { - isTasksLoading.value = true; - + await _executeApiCall(isTasksLoading, () async { final response = await ApiService.getDashboardTasks(projectId: projectId); - - if (response != null && response.success) { - totalTasks.value = response.data?.totalTasks ?? 0; + if (response?.success == true) { + totalTasks.value = response!.data?.totalTasks ?? 0; completedTasks.value = response.data?.completedTasks ?? 0; } else { totalTasks.value = 0; completedTasks.value = 0; } - } finally { - isTasksLoading.value = false; - } + }); } Future fetchDashboardTeams({required String projectId}) async { - try { - isTeamsLoading.value = true; - + await _executeApiCall(isTeamsLoading, () async { final response = await ApiService.getDashboardTeams(projectId: projectId); - - if (response != null && response.success) { - totalEmployees.value = response.data?.totalEmployees ?? 0; + if (response?.success == true) { + totalEmployees.value = response!.data?.totalEmployees ?? 0; inToday.value = response.data?.inToday ?? 0; } else { totalEmployees.value = 0; inToday.value = 0; } - } finally { - isTeamsLoading.value = false; - } + }); } }