383 lines
13 KiB
Dart
383 lines
13 KiB
Dart
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 = <Map<String, dynamic>>[].obs;
|
|
final attendanceSelectedRange = '15D'.obs;
|
|
final attendanceIsChartView = true.obs;
|
|
final isAttendanceLoading = false.obs;
|
|
|
|
final projectChartData = <ChartTaskData>[].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<PendingExpensesData?>(null);
|
|
|
|
final isExpenseTypeReportLoading = false.obs;
|
|
final expenseTypeReportData = Rx<ExpenseTypeReportData?>(null);
|
|
|
|
final expenseReportStartDate =
|
|
DateTime.now().subtract(const Duration(days: 15)).obs;
|
|
final expenseReportEndDate = DateTime.now().obs;
|
|
|
|
final isMonthlyExpenseLoading = false.obs;
|
|
final monthlyExpenseList = <MonthlyExpenseData>[].obs;
|
|
final selectedMonthlyExpenseDuration =
|
|
MonthlyExpenseDuration.twelveMonths.obs;
|
|
final selectedMonthsCount = 12.obs;
|
|
|
|
final expenseTypes = <ExpenseTypeModel>[].obs;
|
|
final selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
|
|
|
final isLoadingEmployees = true.obs;
|
|
final employees = <EmployeeModel>[].obs;
|
|
final uploadingStates = <String, RxBool>{}.obs;
|
|
|
|
final isCollectionOverviewLoading = true.obs;
|
|
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
|
|
|
|
final isPurchaseInvoiceLoading = true.obs;
|
|
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
|
|
|
|
final List<String> 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<String>(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<void> refreshDashboard() =>
|
|
fetchAllDashboardData(projectController.selectedProjectId.value);
|
|
Future<void> refreshAttendance() => fetchRoleWiseAttendance();
|
|
Future<void> refreshProjects() => fetchProjectProgress();
|
|
Future<void> refreshTasks() async {
|
|
final id = projectController.selectedProjectId.value;
|
|
if (id.isNotEmpty) await fetchDashboardTasks(projectId: id);
|
|
}
|
|
|
|
// --------------------------
|
|
// HELPER: Execute API call
|
|
// --------------------------
|
|
Future<void> _executeApiCall(
|
|
RxBool loaderRx, Future<void> 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<void> 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<void> 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<String, dynamic>.from(e)).toList() ?? []);
|
|
});
|
|
}
|
|
|
|
Future<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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,
|
|
}
|