improved dashboard controller
This commit is contained in:
parent
5d73fd6f4f
commit
8686d696f0
@ -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<Map<String, dynamic>> roleWiseData =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
final RxString attendanceSelectedRange = '15D'.obs;
|
||||
final RxBool attendanceIsChartView = true.obs;
|
||||
final RxBool isAttendanceLoading = false.obs;
|
||||
|
||||
// =========================
|
||||
// Project progress overview
|
||||
// =========================
|
||||
final RxList<ChartTaskData> projectChartData = <ChartTaskData>[].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<String> 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?> pendingExpensesData =
|
||||
Rx<PendingExpensesData?>(null);
|
||||
|
||||
// =========================
|
||||
// Expense Category Report
|
||||
// =========================
|
||||
final RxBool isExpenseTypeReportLoading = false.obs;
|
||||
final Rx<ExpenseTypeReportData?> expenseTypeReportData =
|
||||
Rx<ExpenseTypeReportData?>(null);
|
||||
final Rx<DateTime> expenseReportStartDate =
|
||||
// Attendance
|
||||
final roleWiseData = <Map<String, dynamic>>[].obs;
|
||||
final attendanceSelectedRange = '15D'.obs;
|
||||
final attendanceIsChartView = true.obs;
|
||||
final isAttendanceLoading = false.obs;
|
||||
|
||||
// Project Progress
|
||||
final projectChartData = <ChartTaskData>[].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<PendingExpensesData?>(null);
|
||||
|
||||
final isExpenseTypeReportLoading = false.obs;
|
||||
final expenseTypeReportData = Rx<ExpenseTypeReportData?>(null);
|
||||
final expenseReportStartDate =
|
||||
DateTime.now().subtract(const Duration(days: 15)).obs;
|
||||
final Rx<DateTime> expenseReportEndDate = DateTime.now().obs;
|
||||
final expenseReportEndDate = DateTime.now().obs;
|
||||
|
||||
// =========================
|
||||
// Monthly Expense Report
|
||||
// =========================
|
||||
final RxBool isMonthlyExpenseLoading = false.obs;
|
||||
final RxList<MonthlyExpenseData> monthlyExpenseList =
|
||||
<MonthlyExpenseData>[].obs;
|
||||
|
||||
// Filters
|
||||
final Rx<MonthlyExpenseDuration> selectedMonthlyExpenseDuration =
|
||||
final isMonthlyExpenseLoading = false.obs;
|
||||
final monthlyExpenseList = <MonthlyExpenseData>[].obs;
|
||||
final selectedMonthlyExpenseDuration =
|
||||
MonthlyExpenseDuration.twelveMonths.obs;
|
||||
final RxInt selectedMonthsCount = 12.obs;
|
||||
final selectedMonthsCount = 12.obs;
|
||||
|
||||
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
|
||||
final Rx<ExpenseTypeModel?> selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
||||
final expenseTypes = <ExpenseTypeModel>[].obs;
|
||||
final selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
||||
|
||||
// Teams/Employees
|
||||
final isLoadingEmployees = true.obs;
|
||||
final RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
|
||||
final employees = <EmployeeModel>[].obs;
|
||||
final uploadingStates = <String, RxBool>{}.obs;
|
||||
|
||||
// =========================
|
||||
// Collection Overview
|
||||
// =========================
|
||||
final RxBool isCollectionOverviewLoading = false.obs;
|
||||
final Rx<CollectionOverviewData?> collectionOverviewData =
|
||||
Rx<CollectionOverviewData?>(null);
|
||||
// Collection
|
||||
final isCollectionOverviewLoading = false.obs;
|
||||
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
|
||||
|
||||
// Constants
|
||||
final List<String> 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<String>(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<void> refreshDashboard() async => fetchAllDashboardData();
|
||||
Future<void> 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<void> refreshDashboard() => fetchAllDashboardData();
|
||||
Future<void> refreshAttendance() => fetchRoleWiseAttendance();
|
||||
Future<void> refreshProjects() => fetchProjectProgress();
|
||||
Future<void> refreshTasks() async {
|
||||
final id = projectController.selectedProjectId.value;
|
||||
if (id.isNotEmpty) await fetchDashboardTasks(projectId: id);
|
||||
}
|
||||
|
||||
Future<void> refreshProjects() async => fetchProjectProgress();
|
||||
// =========================
|
||||
// 5. DATA FETCHING (API)
|
||||
// =========================
|
||||
|
||||
/// Wrapper to reduce try-finally boilerplate for loading states
|
||||
Future<void> _executeApiCall(
|
||||
RxBool loader, Future<void> Function() apiLogic) async {
|
||||
loader.value = true;
|
||||
try {
|
||||
await apiLogic();
|
||||
} finally {
|
||||
loader.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Fetch All Dashboard Data
|
||||
// =========================
|
||||
Future<void> fetchAllDashboardData() async {
|
||||
final String projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isEmpty) return;
|
||||
@ -232,70 +214,28 @@ class DashboardController extends GetxController {
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// API Calls
|
||||
// =========================
|
||||
|
||||
Future<void> 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<void> fetchTodaysAttendance(String projectId) async {
|
||||
isLoadingEmployees.value = true;
|
||||
|
||||
await _executeApiCall(isLoadingEmployees, () async {
|
||||
final response = await ApiService.getAttendanceForDashboard(projectId);
|
||||
|
||||
if (response != null) {
|
||||
employees.value = response;
|
||||
for (var emp in employees) {
|
||||
uploadingStates[emp.id] = false.obs;
|
||||
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<void> fetchMasterData() async {
|
||||
@ -309,149 +249,96 @@ class DashboardController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> 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<void> 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<void> 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) {
|
||||
id, getAttendanceDays());
|
||||
roleWiseData.value =
|
||||
response.map((e) => Map<String, dynamic>.from(e)).toList();
|
||||
} else {
|
||||
roleWiseData.clear();
|
||||
}
|
||||
} finally {
|
||||
isAttendanceLoading.value = false;
|
||||
}
|
||||
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchExpenseTypeReport({
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
}) async {
|
||||
Future<void> 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<void> 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<void> 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<void> 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user