refactor: optimize project handling and UI state management across controllers and views
This commit is contained in:
parent
4e577bd7eb
commit
81de795e93
@ -12,26 +12,21 @@ 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
|
||||
// --------------------------
|
||||
// STATE VARIABLES
|
||||
// --------------------------
|
||||
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;
|
||||
@ -44,13 +39,12 @@ class DashboardController extends GetxController {
|
||||
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);
|
||||
// OPTIMIZED: Use const Duration for better performance
|
||||
|
||||
final expenseReportStartDate =
|
||||
DateTime.now().subtract(const Duration(days: 15)).obs;
|
||||
final expenseReportEndDate = DateTime.now().obs;
|
||||
@ -64,43 +58,38 @@ class DashboardController extends GetxController {
|
||||
final expenseTypes = <ExpenseTypeModel>[].obs;
|
||||
final selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
||||
|
||||
// Teams/Employees
|
||||
final isLoadingEmployees = true.obs;
|
||||
final employees = <EmployeeModel>[].obs;
|
||||
final uploadingStates = <String, RxBool>{}.obs;
|
||||
|
||||
// Collection
|
||||
final isCollectionOverviewLoading = true.obs;
|
||||
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
|
||||
// =========================
|
||||
// Purchase Invoice Overview
|
||||
// =========================
|
||||
|
||||
final isPurchaseInvoiceLoading = true.obs;
|
||||
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
|
||||
// Constants
|
||||
final List<String> ranges = const [
|
||||
'7D',
|
||||
'15D',
|
||||
'30D'
|
||||
]; // OPTIMIZED: Added const
|
||||
|
||||
final List<String> ranges = const ['7D', '15D', '30D'];
|
||||
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)
|
||||
// =========================
|
||||
// --------------------------
|
||||
// LATEST PROJECT ID (for race condition fix)
|
||||
// --------------------------
|
||||
String _latestProjectId = '';
|
||||
|
||||
// --------------------------
|
||||
// COMPUTED PROPERTIES
|
||||
// --------------------------
|
||||
int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7;
|
||||
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7;
|
||||
|
||||
@ -116,45 +105,46 @@ class DashboardController extends GetxController {
|
||||
return weightedDue / data.totalDueAmount;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 3. LIFECYCLE (No functional change)
|
||||
// =========================
|
||||
|
||||
// --------------------------
|
||||
// LIFECYCLE
|
||||
// --------------------------
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
logSafe('DashboardController initialized', level: LogLevel.info);
|
||||
|
||||
// Project Selection Listener
|
||||
// --------------------------
|
||||
// Project change listener
|
||||
// --------------------------
|
||||
ever<String>(projectController.selectedProjectId, (id) {
|
||||
if (id.isNotEmpty) {
|
||||
fetchAllDashboardData();
|
||||
fetchTodaysAttendance(id);
|
||||
_latestProjectId = id; // track latest project
|
||||
fetchAllDashboardData(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Expense Report Date Listener
|
||||
// OPTIMIZED: Using `everAll` is already efficient for this logic
|
||||
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
|
||||
if (projectController.selectedProjectId.value.isNotEmpty) {
|
||||
final id = projectController.selectedProjectId.value;
|
||||
if (id.isNotEmpty) {
|
||||
fetchExpenseTypeReport(
|
||||
startDate: expenseReportStartDate.value,
|
||||
endDate: expenseReportEndDate.value,
|
||||
projectId: id,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Chart Range Listeners
|
||||
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
|
||||
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 4. USER ACTIONS (No functional change)
|
||||
// =========================
|
||||
|
||||
// --------------------------
|
||||
// USER ACTIONS
|
||||
// --------------------------
|
||||
void updateAttendanceRange(String range) =>
|
||||
attendanceSelectedRange.value = range;
|
||||
|
||||
void updateProjectRange(String range) => projectSelectedRange.value = range;
|
||||
void toggleAttendanceChartView(bool isChart) =>
|
||||
attendanceIsChartView.value = isChart;
|
||||
@ -169,7 +159,6 @@ class DashboardController extends GetxController {
|
||||
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
|
||||
selectedMonthlyExpenseDuration.value = duration;
|
||||
|
||||
// OPTIMIZED: The map approach is highly efficient.
|
||||
const durationMap = {
|
||||
MonthlyExpenseDuration.oneMonth: 1,
|
||||
MonthlyExpenseDuration.threeMonths: 3,
|
||||
@ -182,7 +171,8 @@ class DashboardController extends GetxController {
|
||||
fetchMonthlyExpenses();
|
||||
}
|
||||
|
||||
Future<void> refreshDashboard() => fetchAllDashboardData();
|
||||
Future<void> refreshDashboard() =>
|
||||
fetchAllDashboardData(projectController.selectedProjectId.value);
|
||||
Future<void> refreshAttendance() => fetchRoleWiseAttendance();
|
||||
Future<void> refreshProjects() => fetchProjectProgress();
|
||||
Future<void> refreshTasks() async {
|
||||
@ -190,197 +180,194 @@ class DashboardController extends GetxController {
|
||||
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.
|
||||
// --------------------------
|
||||
// HELPER: Execute API call
|
||||
// --------------------------
|
||||
Future<void> _executeApiCall(
|
||||
RxBool loaderRx, Future<void> 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<void> fetchAllDashboardData() async {
|
||||
final String projectId = projectController.selectedProjectId.value;
|
||||
// --------------------------
|
||||
// API FETCHES
|
||||
// --------------------------
|
||||
Future<void> fetchAllDashboardData(String projectId) async {
|
||||
if (projectId.isEmpty) return;
|
||||
_latestProjectId = projectId;
|
||||
|
||||
// OPTIMIZED: Ensure MasterData is fetched only once if possible, but kept in Future.wait for robustness
|
||||
await Future.wait([
|
||||
fetchRoleWiseAttendance(),
|
||||
fetchProjectProgress(),
|
||||
fetchRoleWiseAttendance(projectId),
|
||||
fetchProjectProgress(projectId),
|
||||
fetchDashboardTasks(projectId: projectId),
|
||||
fetchDashboardTeams(projectId: projectId),
|
||||
fetchPendingExpenses(),
|
||||
fetchPendingExpenses(projectId),
|
||||
fetchExpenseTypeReport(
|
||||
startDate: expenseReportStartDate.value,
|
||||
endDate: expenseReportEndDate.value,
|
||||
projectId: projectId,
|
||||
),
|
||||
fetchMonthlyExpenses(),
|
||||
fetchMonthlyExpenses(projectId: projectId),
|
||||
fetchMasterData(),
|
||||
fetchCollectionOverview(),
|
||||
fetchPurchaseInvoiceOverview(),
|
||||
fetchCollectionOverview(projectId),
|
||||
fetchPurchaseInvoiceOverview(projectId),
|
||||
fetchTodaysAttendance(projectId),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> fetchCollectionOverview() async {
|
||||
final projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isEmpty) return;
|
||||
// --------------------------
|
||||
// Each fetch now ignores stale project responses
|
||||
// --------------------------
|
||||
|
||||
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<void> 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<void> 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<void> 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<void> 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<void> 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<void> fetchRoleWiseAttendance() async {
|
||||
final id = projectController.selectedProjectId.value;
|
||||
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());
|
||||
// OPTIMIZED: Used null-aware assignment
|
||||
roleWiseData.value =
|
||||
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? [];
|
||||
if (_latestProjectId != localId) return; // discard stale response
|
||||
roleWiseData.assignAll(
|
||||
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? []);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> 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<void> fetchProjectProgress() async {
|
||||
final id = projectController.selectedProjectId.value;
|
||||
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.value = response!.data
|
||||
projectChartData.assignAll(response!.data
|
||||
.map((d) => ChartTaskData.fromProjectData(d))
|
||||
.toList();
|
||||
.toList());
|
||||
} else {
|
||||
projectChartData.clear(); // OPTIMIZED: Clear data on failure
|
||||
projectChartData.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchDashboardTasks({required String projectId}) async {
|
||||
final localId = projectId;
|
||||
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;
|
||||
}
|
||||
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 (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;
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -7,14 +7,19 @@ import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
class ProjectController extends GetxController {
|
||||
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;
|
||||
RxString selectedProjectId = ''.obs;
|
||||
RxBool isProjectListExpanded = false.obs;
|
||||
RxBool isProjectSelectionExpanded = false.obs;
|
||||
|
||||
RxBool isProjectSelectionExpanded = false.obs;
|
||||
RxBool isProjectListExpanded = false.obs;
|
||||
RxBool isProjectDropdownExpanded = false.obs;
|
||||
|
||||
RxBool isLoading = true.obs;
|
||||
RxBool isLoadingProjects = true.obs;
|
||||
|
||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||
|
||||
// --------------------------
|
||||
// Current selected project
|
||||
// --------------------------
|
||||
GlobalProjectModel? get selectedProject {
|
||||
if (selectedProjectId.value.isEmpty) return null;
|
||||
return projects.firstWhereOrNull((p) => p.id == selectedProjectId.value);
|
||||
@ -26,58 +31,63 @@ class ProjectController extends GetxController {
|
||||
fetchProjects();
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// Clear all projects & UI states
|
||||
// --------------------------
|
||||
void clearProjects() {
|
||||
projects.clear();
|
||||
selectedProjectId.value = '';
|
||||
|
||||
isProjectSelectionExpanded.value = false;
|
||||
isProjectListExpanded.value = false;
|
||||
isProjectDropdownExpanded.value = false;
|
||||
isLoadingProjects.value = false;
|
||||
isLoading.value = false;
|
||||
isLoadingProjects.value = false;
|
||||
uploadingStates.clear();
|
||||
|
||||
LocalStorage.saveString('selectedProjectId', '');
|
||||
|
||||
logSafe("Projects cleared and UI states reset.");
|
||||
update();
|
||||
}
|
||||
|
||||
/// Fetches projects and initializes selected project.
|
||||
// --------------------------
|
||||
// Fetch projects from API
|
||||
// --------------------------
|
||||
Future<void> fetchProjects() async {
|
||||
isLoadingProjects.value = true;
|
||||
isLoading.value = true;
|
||||
isLoadingProjects.value = true;
|
||||
|
||||
final response = await ApiService.getGlobalProjects();
|
||||
try {
|
||||
final response = await ApiService.getGlobalProjects();
|
||||
|
||||
if (response != null && response.isNotEmpty) {
|
||||
projects.assignAll(
|
||||
response.map((json) => GlobalProjectModel.fromJson(json)).toList(),
|
||||
);
|
||||
if (response != null && response.isNotEmpty) {
|
||||
projects.assignAll(response.map((json) => GlobalProjectModel.fromJson(json)).toList());
|
||||
|
||||
String? savedId = LocalStorage.getString('selectedProjectId');
|
||||
if (savedId != null && projects.any((p) => p.id == savedId)) {
|
||||
selectedProjectId.value = savedId;
|
||||
// Load previously saved project
|
||||
String? savedId = LocalStorage.getString('selectedProjectId');
|
||||
if (savedId != null && projects.any((p) => p.id == savedId)) {
|
||||
selectedProjectId.value = savedId;
|
||||
} else {
|
||||
selectedProjectId.value = projects.first.id.toString();
|
||||
LocalStorage.saveString('selectedProjectId', selectedProjectId.value);
|
||||
}
|
||||
|
||||
logSafe("Projects fetched: ${projects.length}");
|
||||
} else {
|
||||
selectedProjectId.value = projects.first.id.toString();
|
||||
LocalStorage.saveString('selectedProjectId', selectedProjectId.value);
|
||||
logSafe("No projects found or API call failed.", level: LogLevel.warning);
|
||||
}
|
||||
|
||||
isProjectSelectionExpanded.value = false;
|
||||
logSafe("Projects fetched: ${projects.length}");
|
||||
} else {
|
||||
logSafe("No Global projects found or API call failed.", level: LogLevel.warning);
|
||||
} catch (e, stack) {
|
||||
logSafe("Error fetching projects: $e", level: LogLevel.error, stackTrace: stack);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isLoadingProjects.value = false;
|
||||
}
|
||||
|
||||
isLoadingProjects.value = false;
|
||||
isLoading.value = false;
|
||||
update(['dashboard_controller']);
|
||||
}
|
||||
|
||||
Future<void> updateSelectedProject(String projectId) async {
|
||||
if (selectedProjectId.value == projectId) return;
|
||||
selectedProjectId.value = projectId;
|
||||
await LocalStorage.saveString('selectedProjectId', projectId);
|
||||
logSafe("Selected project updated to $projectId");
|
||||
update(['selected_project']);
|
||||
isProjectSelectionExpanded.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,6 +70,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
|
||||
appBar: CustomAppBar(
|
||||
title: 'Contact Profile',
|
||||
backgroundColor: appBarColor,
|
||||
projectName: " All Projects",
|
||||
onBackPressed: () => Get.offAllNamed('/dashboard/directory-main-page'),
|
||||
),
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
|
||||
appBar: CustomAppBar(
|
||||
title: "Directory",
|
||||
onBackPressed: () => Get.offNamed('/dashboard'),
|
||||
projectName: " All Projects",
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
body: Stack(
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:on_field_work/view/employees/employee_detail_screen.dart';
|
||||
import 'package:on_field_work/view/document/user_document_screen.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart'; // <-- import PillTabBar
|
||||
|
||||
class EmployeeProfilePage extends StatefulWidget {
|
||||
final String employeeId;
|
||||
@ -16,14 +17,11 @@ class EmployeeProfilePage extends StatefulWidget {
|
||||
|
||||
class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
||||
with SingleTickerProviderStateMixin, UIMixin {
|
||||
// We no longer need to listen to the TabController for setState,
|
||||
// as the TabBar handles its own state updates via the controller.
|
||||
late TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize TabController with 2 tabs
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@ -33,11 +31,8 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- No need for _buildSegmentedButton function anymore ---
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Accessing theme colors for consistency
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
final Color primaryColor = contentTheme.primary;
|
||||
|
||||
@ -45,13 +40,13 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
||||
backgroundColor: const Color(0xFFF1F1F1),
|
||||
appBar: CustomAppBar(
|
||||
title: "Employee Profile",
|
||||
projectName: " All Projects",
|
||||
onBackPressed: () => Get.back(),
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// === Gradient at the top behind AppBar + Toggle ===
|
||||
// This container ensures the background color transitions nicely
|
||||
// Gradient at the top behind AppBar + Toggle
|
||||
Container(
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
@ -65,63 +60,20 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
||||
),
|
||||
),
|
||||
),
|
||||
// === Main Content Area ===
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
// 🛑 NEW: The Modern TabBar Implementation 🛑
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Container(
|
||||
height: 48, // Define a specific height for the TabBar container
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(24.0), // Rounded corners for a chip-like look
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.15),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
// Style the indicator as a subtle pill/chip
|
||||
indicator: BoxDecoration(
|
||||
color: primaryColor.withOpacity(0.1), // Light background color for the selection
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
// The padding is used to slightly shrink the indicator area
|
||||
indicatorPadding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
|
||||
|
||||
// Text styling
|
||||
labelColor: primaryColor, // Selected text color is primary
|
||||
unselectedLabelColor: Colors.grey.shade600, // Unselected text color is darker grey
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15,
|
||||
),
|
||||
|
||||
// Tabs (No custom widget needed, just use the built-in Tab)
|
||||
tabs: const [
|
||||
Tab(text: "Details"),
|
||||
Tab(text: "Documents"),
|
||||
],
|
||||
// Setting this to zero removes the default underline
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
PillTabBar(
|
||||
controller: _tabController,
|
||||
tabs: const ["Details", "Documents"],
|
||||
icons: const [Icons.person, Icons.folder],
|
||||
selectedColor: primaryColor,
|
||||
unselectedColor: Colors.grey.shade600,
|
||||
indicatorColor: primaryColor,
|
||||
height: 48,
|
||||
),
|
||||
|
||||
// 🛑 TabBarView (The Content) 🛑
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
@ -144,4 +96,4 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
import 'package:on_field_work/model/employees/add_employee_bottom_sheet.dart';
|
||||
import 'package:on_field_work/controller/employee/employees_screen_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/avatar.dart';
|
||||
import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
|
||||
import 'package:on_field_work/helpers/utils/launcher_utils.dart';
|
||||
import 'package:on_field_work/view/employees/assign_employee_bottom_sheet.dart';
|
||||
@ -91,8 +90,7 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
appBar: CustomAppBar(
|
||||
title: "Employees",
|
||||
backgroundColor: appBarColor,
|
||||
projectName: Get.find<ProjectController>().selectedProject?.name ??
|
||||
'Select Project',
|
||||
projectName: " All Projects",
|
||||
onBackPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
body: Stack(
|
||||
|
||||
@ -42,7 +42,7 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
||||
super.initState();
|
||||
controller = Get.put(ExpenseDetailController(), tag: widget.expenseId);
|
||||
// EmployeeInfo loading and permission checking is now handled inside controller.init()
|
||||
controller.init(widget.expenseId);
|
||||
controller.init(widget.expenseId);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -61,6 +61,7 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
||||
backgroundColor: const Color(0xFFF7F7F7),
|
||||
appBar: CustomAppBar(
|
||||
title: "Expense Details",
|
||||
projectName: " All Projects",
|
||||
backgroundColor: appBarColor,
|
||||
onBackPressed: () => Get.toNamed('/dashboard/expense-main-page'),
|
||||
),
|
||||
@ -270,9 +271,8 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
||||
controller.parsePermissionIds(rawPermissions);
|
||||
|
||||
final isSubmitStatus = next.id == submitStatusId;
|
||||
final isCreatedByCurrentUser =
|
||||
controller.employeeInfo?.id == expense.createdBy.id; // Use controller's employeeInfo
|
||||
|
||||
final isCreatedByCurrentUser = controller.employeeInfo?.id ==
|
||||
expense.createdBy.id;
|
||||
if (isSubmitStatus) return isCreatedByCurrentUser;
|
||||
return permissionController.hasAnyPermission(parsedPermissions);
|
||||
}).map((next) {
|
||||
@ -311,8 +311,7 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.vertical(top: Radius.circular(5))),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(5))),
|
||||
builder: (context) => ReimbursementBottomSheet(
|
||||
expenseId: expense.id,
|
||||
statusId: next.id,
|
||||
@ -785,4 +784,4 @@ class _InvoiceTotals extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,6 +95,7 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
|
||||
backgroundColor: Colors.white,
|
||||
appBar: CustomAppBar(
|
||||
title: "Expense & Reimbursement",
|
||||
projectName: " All Projects",
|
||||
backgroundColor: appBarColor,
|
||||
onBackPressed: () => Get.toNamed('/dashboard/finance'),
|
||||
),
|
||||
|
||||
@ -54,6 +54,7 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: CustomAppBar(
|
||||
title: "Advance Payments",
|
||||
projectName: " All Projects",
|
||||
onBackPressed: () => Get.offNamed('/dashboard/finance'),
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
|
||||
@ -14,7 +14,6 @@ import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
|
||||
|
||||
class FinanceScreen extends StatefulWidget {
|
||||
const FinanceScreen({super.key});
|
||||
|
||||
@ -59,7 +58,8 @@ class _FinanceScreenState extends State<FinanceScreen>
|
||||
backgroundColor: const Color(0xFFF8F9FA),
|
||||
appBar: CustomAppBar(
|
||||
title: "Finance",
|
||||
onBackPressed: () => Get.offAllNamed( '/dashboard' ),
|
||||
projectName: " All Projects",
|
||||
onBackPressed: () => Get.offAllNamed('/dashboard'),
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
body: Stack(
|
||||
|
||||
@ -114,6 +114,7 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
|
||||
backgroundColor: Colors.white,
|
||||
appBar: CustomAppBar(
|
||||
title: "Payment Request Details",
|
||||
projectName: " All Projects",
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
body: Stack(
|
||||
@ -217,7 +218,7 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (!_checkedPermission && employeeInfo != null) {
|
||||
if (!_checkedPermission && employeeInfo != null) {
|
||||
_checkedPermission = true;
|
||||
_checkPermissionToSubmit(request);
|
||||
}
|
||||
|
||||
@ -104,6 +104,7 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
||||
backgroundColor: Colors.white,
|
||||
appBar: CustomAppBar(
|
||||
title: "Payment Requests",
|
||||
projectName: " All Projects",
|
||||
onBackPressed: () => Get.offNamed('/dashboard/finance'),
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user