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';
|
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
|
||||||
|
|
||||||
class DashboardController extends GetxController {
|
class DashboardController extends GetxController {
|
||||||
// Dependencies
|
|
||||||
final ProjectController projectController = Get.put(ProjectController());
|
final ProjectController projectController = Get.put(ProjectController());
|
||||||
|
|
||||||
// =========================
|
// --------------------------
|
||||||
// 1. STATE VARIABLES (No functional change)
|
// STATE VARIABLES
|
||||||
// =========================
|
// --------------------------
|
||||||
|
|
||||||
// Attendance
|
|
||||||
final roleWiseData = <Map<String, dynamic>>[].obs;
|
final roleWiseData = <Map<String, dynamic>>[].obs;
|
||||||
final attendanceSelectedRange = '15D'.obs;
|
final attendanceSelectedRange = '15D'.obs;
|
||||||
final attendanceIsChartView = true.obs;
|
final attendanceIsChartView = true.obs;
|
||||||
final isAttendanceLoading = false.obs;
|
final isAttendanceLoading = false.obs;
|
||||||
|
|
||||||
// Project Progress
|
|
||||||
final projectChartData = <ChartTaskData>[].obs;
|
final projectChartData = <ChartTaskData>[].obs;
|
||||||
final projectSelectedRange = '15D'.obs;
|
final projectSelectedRange = '15D'.obs;
|
||||||
final projectIsChartView = true.obs;
|
final projectIsChartView = true.obs;
|
||||||
final isProjectLoading = false.obs;
|
final isProjectLoading = false.obs;
|
||||||
|
|
||||||
// Overview Counts
|
|
||||||
final totalProjects = 0.obs;
|
final totalProjects = 0.obs;
|
||||||
final ongoingProjects = 0.obs;
|
final ongoingProjects = 0.obs;
|
||||||
final isProjectsLoading = false.obs;
|
final isProjectsLoading = false.obs;
|
||||||
@ -44,13 +39,12 @@ class DashboardController extends GetxController {
|
|||||||
final inToday = 0.obs;
|
final inToday = 0.obs;
|
||||||
final isTeamsLoading = false.obs;
|
final isTeamsLoading = false.obs;
|
||||||
|
|
||||||
// Expenses & Reports
|
|
||||||
final isPendingExpensesLoading = false.obs;
|
final isPendingExpensesLoading = false.obs;
|
||||||
final pendingExpensesData = Rx<PendingExpensesData?>(null);
|
final pendingExpensesData = Rx<PendingExpensesData?>(null);
|
||||||
|
|
||||||
final isExpenseTypeReportLoading = false.obs;
|
final isExpenseTypeReportLoading = false.obs;
|
||||||
final expenseTypeReportData = Rx<ExpenseTypeReportData?>(null);
|
final expenseTypeReportData = Rx<ExpenseTypeReportData?>(null);
|
||||||
// OPTIMIZED: Use const Duration for better performance
|
|
||||||
final expenseReportStartDate =
|
final expenseReportStartDate =
|
||||||
DateTime.now().subtract(const Duration(days: 15)).obs;
|
DateTime.now().subtract(const Duration(days: 15)).obs;
|
||||||
final expenseReportEndDate = DateTime.now().obs;
|
final expenseReportEndDate = DateTime.now().obs;
|
||||||
@ -64,43 +58,38 @@ class DashboardController extends GetxController {
|
|||||||
final expenseTypes = <ExpenseTypeModel>[].obs;
|
final expenseTypes = <ExpenseTypeModel>[].obs;
|
||||||
final selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
final selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
||||||
|
|
||||||
// Teams/Employees
|
|
||||||
final isLoadingEmployees = true.obs;
|
final isLoadingEmployees = true.obs;
|
||||||
final employees = <EmployeeModel>[].obs;
|
final employees = <EmployeeModel>[].obs;
|
||||||
final uploadingStates = <String, RxBool>{}.obs;
|
final uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|
||||||
// Collection
|
|
||||||
final isCollectionOverviewLoading = true.obs;
|
final isCollectionOverviewLoading = true.obs;
|
||||||
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
|
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
|
||||||
// =========================
|
|
||||||
// Purchase Invoice Overview
|
|
||||||
// =========================
|
|
||||||
final isPurchaseInvoiceLoading = true.obs;
|
final isPurchaseInvoiceLoading = true.obs;
|
||||||
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
|
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
|
||||||
// Constants
|
|
||||||
final List<String> ranges = const [
|
final List<String> ranges = const ['7D', '15D', '30D'];
|
||||||
'7D',
|
|
||||||
'15D',
|
|
||||||
'30D'
|
|
||||||
]; // OPTIMIZED: Added const
|
|
||||||
static const _rangeDaysMap = {
|
static const _rangeDaysMap = {
|
||||||
// OPTIMIZED: Added const
|
|
||||||
'7D': 7,
|
'7D': 7,
|
||||||
'15D': 15,
|
'15D': 15,
|
||||||
'30D': 30,
|
'30D': 30,
|
||||||
'3M': 90,
|
'3M': 90,
|
||||||
'6M': 180
|
'6M': 180
|
||||||
};
|
};
|
||||||
// DSO Calculation Constants (OPTIMIZED: Added const)
|
|
||||||
static const double _w0_30 = 15.0;
|
static const double _w0_30 = 15.0;
|
||||||
static const double _w30_60 = 45.0;
|
static const double _w30_60 = 45.0;
|
||||||
static const double _w60_90 = 75.0;
|
static const double _w60_90 = 75.0;
|
||||||
static const double _w90_plus = 105.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 getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7;
|
||||||
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7;
|
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7;
|
||||||
|
|
||||||
@ -116,45 +105,46 @@ class DashboardController extends GetxController {
|
|||||||
return weightedDue / data.totalDueAmount;
|
return weightedDue / data.totalDueAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// --------------------------
|
||||||
// 3. LIFECYCLE (No functional change)
|
// LIFECYCLE
|
||||||
// =========================
|
// --------------------------
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
logSafe('DashboardController initialized', level: LogLevel.info);
|
logSafe('DashboardController initialized', level: LogLevel.info);
|
||||||
|
|
||||||
// Project Selection Listener
|
// --------------------------
|
||||||
|
// Project change listener
|
||||||
|
// --------------------------
|
||||||
ever<String>(projectController.selectedProjectId, (id) {
|
ever<String>(projectController.selectedProjectId, (id) {
|
||||||
if (id.isNotEmpty) {
|
if (id.isNotEmpty) {
|
||||||
fetchAllDashboardData();
|
_latestProjectId = id; // track latest project
|
||||||
fetchTodaysAttendance(id);
|
fetchAllDashboardData(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Expense Report Date Listener
|
// Expense Report Date Listener
|
||||||
// OPTIMIZED: Using `everAll` is already efficient for this logic
|
|
||||||
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
|
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
|
||||||
if (projectController.selectedProjectId.value.isNotEmpty) {
|
final id = projectController.selectedProjectId.value;
|
||||||
|
if (id.isNotEmpty) {
|
||||||
fetchExpenseTypeReport(
|
fetchExpenseTypeReport(
|
||||||
startDate: expenseReportStartDate.value,
|
startDate: expenseReportStartDate.value,
|
||||||
endDate: expenseReportEndDate.value,
|
endDate: expenseReportEndDate.value,
|
||||||
|
projectId: id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Chart Range Listeners
|
|
||||||
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
|
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
|
||||||
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// --------------------------
|
||||||
// 4. USER ACTIONS (No functional change)
|
// USER ACTIONS
|
||||||
// =========================
|
// --------------------------
|
||||||
|
|
||||||
void updateAttendanceRange(String range) =>
|
void updateAttendanceRange(String range) =>
|
||||||
attendanceSelectedRange.value = range;
|
attendanceSelectedRange.value = range;
|
||||||
|
|
||||||
void updateProjectRange(String range) => projectSelectedRange.value = range;
|
void updateProjectRange(String range) => projectSelectedRange.value = range;
|
||||||
void toggleAttendanceChartView(bool isChart) =>
|
void toggleAttendanceChartView(bool isChart) =>
|
||||||
attendanceIsChartView.value = isChart;
|
attendanceIsChartView.value = isChart;
|
||||||
@ -169,7 +159,6 @@ class DashboardController extends GetxController {
|
|||||||
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
|
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
|
||||||
selectedMonthlyExpenseDuration.value = duration;
|
selectedMonthlyExpenseDuration.value = duration;
|
||||||
|
|
||||||
// OPTIMIZED: The map approach is highly efficient.
|
|
||||||
const durationMap = {
|
const durationMap = {
|
||||||
MonthlyExpenseDuration.oneMonth: 1,
|
MonthlyExpenseDuration.oneMonth: 1,
|
||||||
MonthlyExpenseDuration.threeMonths: 3,
|
MonthlyExpenseDuration.threeMonths: 3,
|
||||||
@ -182,7 +171,8 @@ class DashboardController extends GetxController {
|
|||||||
fetchMonthlyExpenses();
|
fetchMonthlyExpenses();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshDashboard() => fetchAllDashboardData();
|
Future<void> refreshDashboard() =>
|
||||||
|
fetchAllDashboardData(projectController.selectedProjectId.value);
|
||||||
Future<void> refreshAttendance() => fetchRoleWiseAttendance();
|
Future<void> refreshAttendance() => fetchRoleWiseAttendance();
|
||||||
Future<void> refreshProjects() => fetchProjectProgress();
|
Future<void> refreshProjects() => fetchProjectProgress();
|
||||||
Future<void> refreshTasks() async {
|
Future<void> refreshTasks() async {
|
||||||
@ -190,197 +180,194 @@ class DashboardController extends GetxController {
|
|||||||
if (id.isNotEmpty) await fetchDashboardTasks(projectId: id);
|
if (id.isNotEmpty) await fetchDashboardTasks(projectId: id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// --------------------------
|
||||||
// 5. DATA FETCHING (API)
|
// HELPER: Execute API call
|
||||||
// =========================
|
// --------------------------
|
||||||
|
|
||||||
/// Wrapper to reduce try-finally boilerplate for loading states
|
|
||||||
// OPTIMIZED: Renamed variable to avoid shadowing standard library.
|
|
||||||
Future<void> _executeApiCall(
|
Future<void> _executeApiCall(
|
||||||
RxBool loaderRx, Future<void> Function() apiLogic) async {
|
RxBool loaderRx, Future<void> Function() apiLogic) async {
|
||||||
loaderRx.value = true;
|
loaderRx.value = true;
|
||||||
try {
|
try {
|
||||||
await apiLogic();
|
await apiLogic();
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
// OPTIMIZED: Added logging of error for better debugging
|
|
||||||
logSafe('API Call Failed: $e', level: LogLevel.error, stackTrace: stack);
|
logSafe('API Call Failed: $e', level: LogLevel.error, stackTrace: stack);
|
||||||
} finally {
|
} finally {
|
||||||
loaderRx.value = false;
|
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;
|
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([
|
await Future.wait([
|
||||||
fetchRoleWiseAttendance(),
|
fetchRoleWiseAttendance(projectId),
|
||||||
fetchProjectProgress(),
|
fetchProjectProgress(projectId),
|
||||||
fetchDashboardTasks(projectId: projectId),
|
fetchDashboardTasks(projectId: projectId),
|
||||||
fetchDashboardTeams(projectId: projectId),
|
fetchDashboardTeams(projectId: projectId),
|
||||||
fetchPendingExpenses(),
|
fetchPendingExpenses(projectId),
|
||||||
fetchExpenseTypeReport(
|
fetchExpenseTypeReport(
|
||||||
startDate: expenseReportStartDate.value,
|
startDate: expenseReportStartDate.value,
|
||||||
endDate: expenseReportEndDate.value,
|
endDate: expenseReportEndDate.value,
|
||||||
|
projectId: projectId,
|
||||||
),
|
),
|
||||||
fetchMonthlyExpenses(),
|
fetchMonthlyExpenses(projectId: projectId),
|
||||||
fetchMasterData(),
|
fetchMasterData(),
|
||||||
fetchCollectionOverview(),
|
fetchCollectionOverview(projectId),
|
||||||
fetchPurchaseInvoiceOverview(),
|
fetchPurchaseInvoiceOverview(projectId),
|
||||||
fetchTodaysAttendance(projectId),
|
fetchTodaysAttendance(projectId),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchCollectionOverview() async {
|
// --------------------------
|
||||||
final projectId = projectController.selectedProjectId.value;
|
// Each fetch now ignores stale project responses
|
||||||
if (projectId.isEmpty) return;
|
// --------------------------
|
||||||
|
|
||||||
await _executeApiCall(isCollectionOverviewLoading, () async {
|
Future<void> fetchRoleWiseAttendance([String? projectId]) async {
|
||||||
final response =
|
final id = projectId ?? projectController.selectedProjectId.value;
|
||||||
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;
|
|
||||||
if (id.isEmpty) return;
|
if (id.isEmpty) return;
|
||||||
|
|
||||||
|
final localId = id;
|
||||||
await _executeApiCall(isAttendanceLoading, () async {
|
await _executeApiCall(isAttendanceLoading, () async {
|
||||||
final response = await ApiService.getDashboardAttendanceOverview(
|
final response = await ApiService.getDashboardAttendanceOverview(
|
||||||
id, getAttendanceDays());
|
id, getAttendanceDays());
|
||||||
// OPTIMIZED: Used null-aware assignment
|
if (_latestProjectId != localId) return; // discard stale response
|
||||||
roleWiseData.value =
|
roleWiseData.assignAll(
|
||||||
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? [];
|
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? []);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchExpenseTypeReport(
|
Future<void> fetchProjectProgress([String? projectId]) async {
|
||||||
{required DateTime startDate, required DateTime endDate}) async {
|
final id = projectId ?? projectController.selectedProjectId.value;
|
||||||
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;
|
|
||||||
if (id.isEmpty) return;
|
if (id.isEmpty) return;
|
||||||
|
|
||||||
|
final localId = id;
|
||||||
await _executeApiCall(isProjectLoading, () async {
|
await _executeApiCall(isProjectLoading, () async {
|
||||||
final response = await ApiService.getProjectProgress(
|
final response = await ApiService.getProjectProgress(
|
||||||
projectId: id, days: getProjectDays());
|
projectId: id, days: getProjectDays());
|
||||||
|
if (_latestProjectId != localId) return;
|
||||||
if (response?.success == true) {
|
if (response?.success == true) {
|
||||||
projectChartData.value = response!.data
|
projectChartData.assignAll(response!.data
|
||||||
.map((d) => ChartTaskData.fromProjectData(d))
|
.map((d) => ChartTaskData.fromProjectData(d))
|
||||||
.toList();
|
.toList());
|
||||||
} else {
|
} else {
|
||||||
projectChartData.clear(); // OPTIMIZED: Clear data on failure
|
projectChartData.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchDashboardTasks({required String projectId}) async {
|
Future<void> fetchDashboardTasks({required String projectId}) async {
|
||||||
|
final localId = projectId;
|
||||||
await _executeApiCall(isTasksLoading, () async {
|
await _executeApiCall(isTasksLoading, () async {
|
||||||
final response = await ApiService.getDashboardTasks(projectId: projectId);
|
final response = await ApiService.getDashboardTasks(projectId: projectId);
|
||||||
if (response?.success == true) {
|
if (_latestProjectId != localId) return;
|
||||||
// OPTIMIZED: Used null-aware access with default value
|
totalTasks.value = response?.data?.totalTasks ?? 0;
|
||||||
totalTasks.value = response!.data?.totalTasks ?? 0;
|
completedTasks.value = response?.data?.completedTasks ?? 0;
|
||||||
completedTasks.value = response.data?.completedTasks ?? 0;
|
|
||||||
} else {
|
|
||||||
totalTasks.value = 0;
|
|
||||||
completedTasks.value = 0;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchDashboardTeams({required String projectId}) async {
|
Future<void> fetchDashboardTeams({required String projectId}) async {
|
||||||
|
final localId = projectId;
|
||||||
await _executeApiCall(isTeamsLoading, () async {
|
await _executeApiCall(isTeamsLoading, () async {
|
||||||
final response = await ApiService.getDashboardTeams(projectId: projectId);
|
final response = await ApiService.getDashboardTeams(projectId: projectId);
|
||||||
if (response?.success == true) {
|
if (_latestProjectId != localId) return;
|
||||||
// OPTIMIZED: Used null-aware access with default value
|
totalEmployees.value = response?.data?.totalEmployees ?? 0;
|
||||||
totalEmployees.value = response!.data?.totalEmployees ?? 0;
|
inToday.value = response?.data?.inToday ?? 0;
|
||||||
inToday.value = response.data?.inToday ?? 0;
|
});
|
||||||
} else {
|
}
|
||||||
totalEmployees.value = 0;
|
|
||||||
inToday.value = 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 {
|
class ProjectController extends GetxController {
|
||||||
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;
|
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;
|
||||||
RxString selectedProjectId = ''.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 isProjectDropdownExpanded = false.obs;
|
||||||
|
|
||||||
RxBool isLoading = true.obs;
|
RxBool isLoading = true.obs;
|
||||||
RxBool isLoadingProjects = true.obs;
|
RxBool isLoadingProjects = true.obs;
|
||||||
|
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// Current selected project
|
||||||
|
// --------------------------
|
||||||
GlobalProjectModel? get selectedProject {
|
GlobalProjectModel? get selectedProject {
|
||||||
if (selectedProjectId.value.isEmpty) return null;
|
if (selectedProjectId.value.isEmpty) return null;
|
||||||
return projects.firstWhereOrNull((p) => p.id == selectedProjectId.value);
|
return projects.firstWhereOrNull((p) => p.id == selectedProjectId.value);
|
||||||
@ -26,58 +31,63 @@ class ProjectController extends GetxController {
|
|||||||
fetchProjects();
|
fetchProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// Clear all projects & UI states
|
||||||
|
// --------------------------
|
||||||
void clearProjects() {
|
void clearProjects() {
|
||||||
projects.clear();
|
projects.clear();
|
||||||
selectedProjectId.value = '';
|
selectedProjectId.value = '';
|
||||||
|
|
||||||
isProjectSelectionExpanded.value = false;
|
isProjectSelectionExpanded.value = false;
|
||||||
isProjectListExpanded.value = false;
|
isProjectListExpanded.value = false;
|
||||||
isProjectDropdownExpanded.value = false;
|
isProjectDropdownExpanded.value = false;
|
||||||
isLoadingProjects.value = false;
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
isLoadingProjects.value = false;
|
||||||
uploadingStates.clear();
|
uploadingStates.clear();
|
||||||
|
|
||||||
LocalStorage.saveString('selectedProjectId', '');
|
LocalStorage.saveString('selectedProjectId', '');
|
||||||
|
|
||||||
logSafe("Projects cleared and UI states reset.");
|
logSafe("Projects cleared and UI states reset.");
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches projects and initializes selected project.
|
// --------------------------
|
||||||
|
// Fetch projects from API
|
||||||
|
// --------------------------
|
||||||
Future<void> fetchProjects() async {
|
Future<void> fetchProjects() async {
|
||||||
isLoadingProjects.value = true;
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
isLoadingProjects.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getGlobalProjects();
|
try {
|
||||||
|
final response = await ApiService.getGlobalProjects();
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
projects.assignAll(
|
projects.assignAll(response.map((json) => GlobalProjectModel.fromJson(json)).toList());
|
||||||
response.map((json) => GlobalProjectModel.fromJson(json)).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
String? savedId = LocalStorage.getString('selectedProjectId');
|
// Load previously saved project
|
||||||
if (savedId != null && projects.any((p) => p.id == savedId)) {
|
String? savedId = LocalStorage.getString('selectedProjectId');
|
||||||
selectedProjectId.value = savedId;
|
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 {
|
} else {
|
||||||
selectedProjectId.value = projects.first.id.toString();
|
logSafe("No projects found or API call failed.", level: LogLevel.warning);
|
||||||
LocalStorage.saveString('selectedProjectId', selectedProjectId.value);
|
|
||||||
}
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
isProjectSelectionExpanded.value = false;
|
logSafe("Error fetching projects: $e", level: LogLevel.error, stackTrace: stack);
|
||||||
logSafe("Projects fetched: ${projects.length}");
|
} finally {
|
||||||
} else {
|
isLoading.value = false;
|
||||||
logSafe("No Global projects found or API call failed.", level: LogLevel.warning);
|
isLoadingProjects.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingProjects.value = false;
|
|
||||||
isLoading.value = false;
|
|
||||||
update(['dashboard_controller']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateSelectedProject(String projectId) async {
|
Future<void> updateSelectedProject(String projectId) async {
|
||||||
|
if (selectedProjectId.value == projectId) return;
|
||||||
selectedProjectId.value = projectId;
|
selectedProjectId.value = projectId;
|
||||||
await LocalStorage.saveString('selectedProjectId', projectId);
|
await LocalStorage.saveString('selectedProjectId', projectId);
|
||||||
logSafe("Selected project updated to $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(
|
appBar: CustomAppBar(
|
||||||
title: 'Contact Profile',
|
title: 'Contact Profile',
|
||||||
backgroundColor: appBarColor,
|
backgroundColor: appBarColor,
|
||||||
|
projectName: " All Projects",
|
||||||
onBackPressed: () => Get.offAllNamed('/dashboard/directory-main-page'),
|
onBackPressed: () => Get.offAllNamed('/dashboard/directory-main-page'),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,7 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
|
|||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: "Directory",
|
title: "Directory",
|
||||||
onBackPressed: () => Get.offNamed('/dashboard'),
|
onBackPressed: () => Get.offNamed('/dashboard'),
|
||||||
|
projectName: " All Projects",
|
||||||
backgroundColor: appBarColor,
|
backgroundColor: appBarColor,
|
||||||
),
|
),
|
||||||
body: Stack(
|
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/view/document/user_document_screen.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.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/utils/mixins/ui_mixin.dart';
|
||||||
|
import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart'; // <-- import PillTabBar
|
||||||
|
|
||||||
class EmployeeProfilePage extends StatefulWidget {
|
class EmployeeProfilePage extends StatefulWidget {
|
||||||
final String employeeId;
|
final String employeeId;
|
||||||
@ -16,14 +17,11 @@ class EmployeeProfilePage extends StatefulWidget {
|
|||||||
|
|
||||||
class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
||||||
with SingleTickerProviderStateMixin, UIMixin {
|
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;
|
late TabController _tabController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Initialize TabController with 2 tabs
|
|
||||||
_tabController = TabController(length: 2, vsync: this);
|
_tabController = TabController(length: 2, vsync: this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,11 +31,8 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- No need for _buildSegmentedButton function anymore ---
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Accessing theme colors for consistency
|
|
||||||
final Color appBarColor = contentTheme.primary;
|
final Color appBarColor = contentTheme.primary;
|
||||||
final Color primaryColor = contentTheme.primary;
|
final Color primaryColor = contentTheme.primary;
|
||||||
|
|
||||||
@ -45,13 +40,13 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
|||||||
backgroundColor: const Color(0xFFF1F1F1),
|
backgroundColor: const Color(0xFFF1F1F1),
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: "Employee Profile",
|
title: "Employee Profile",
|
||||||
|
projectName: " All Projects",
|
||||||
onBackPressed: () => Get.back(),
|
onBackPressed: () => Get.back(),
|
||||||
backgroundColor: appBarColor,
|
backgroundColor: appBarColor,
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// === Gradient at the top behind AppBar + Toggle ===
|
// Gradient at the top behind AppBar + Toggle
|
||||||
// This container ensures the background color transitions nicely
|
|
||||||
Container(
|
Container(
|
||||||
height: 50,
|
height: 50,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -65,63 +60,20 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// === Main Content Area ===
|
|
||||||
SafeArea(
|
SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
bottom: true,
|
bottom: true,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 🛑 NEW: The Modern TabBar Implementation 🛑
|
PillTabBar(
|
||||||
Padding(
|
controller: _tabController,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
tabs: const ["Details", "Documents"],
|
||||||
child: Container(
|
icons: const [Icons.person, Icons.folder],
|
||||||
height: 48, // Define a specific height for the TabBar container
|
selectedColor: primaryColor,
|
||||||
decoration: BoxDecoration(
|
unselectedColor: Colors.grey.shade600,
|
||||||
color: Colors.white,
|
indicatorColor: primaryColor,
|
||||||
borderRadius: BorderRadius.circular(24.0), // Rounded corners for a chip-like look
|
height: 48,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// 🛑 TabBarView (The Content) 🛑
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
controller: _tabController,
|
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/model/employees/add_employee_bottom_sheet.dart';
|
||||||
import 'package:on_field_work/controller/employee/employees_screen_controller.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/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/widgets/my_custom_skeleton.dart';
|
||||||
import 'package:on_field_work/helpers/utils/launcher_utils.dart';
|
import 'package:on_field_work/helpers/utils/launcher_utils.dart';
|
||||||
import 'package:on_field_work/view/employees/assign_employee_bottom_sheet.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(
|
appBar: CustomAppBar(
|
||||||
title: "Employees",
|
title: "Employees",
|
||||||
backgroundColor: appBarColor,
|
backgroundColor: appBarColor,
|
||||||
projectName: Get.find<ProjectController>().selectedProject?.name ??
|
projectName: " All Projects",
|
||||||
'Select Project',
|
|
||||||
onBackPressed: () => Get.offNamed('/dashboard'),
|
onBackPressed: () => Get.offNamed('/dashboard'),
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
|
|||||||
@ -42,7 +42,7 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
|||||||
super.initState();
|
super.initState();
|
||||||
controller = Get.put(ExpenseDetailController(), tag: widget.expenseId);
|
controller = Get.put(ExpenseDetailController(), tag: widget.expenseId);
|
||||||
// EmployeeInfo loading and permission checking is now handled inside controller.init()
|
// EmployeeInfo loading and permission checking is now handled inside controller.init()
|
||||||
controller.init(widget.expenseId);
|
controller.init(widget.expenseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -61,6 +61,7 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
|||||||
backgroundColor: const Color(0xFFF7F7F7),
|
backgroundColor: const Color(0xFFF7F7F7),
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: "Expense Details",
|
title: "Expense Details",
|
||||||
|
projectName: " All Projects",
|
||||||
backgroundColor: appBarColor,
|
backgroundColor: appBarColor,
|
||||||
onBackPressed: () => Get.toNamed('/dashboard/expense-main-page'),
|
onBackPressed: () => Get.toNamed('/dashboard/expense-main-page'),
|
||||||
),
|
),
|
||||||
@ -270,9 +271,8 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
|||||||
controller.parsePermissionIds(rawPermissions);
|
controller.parsePermissionIds(rawPermissions);
|
||||||
|
|
||||||
final isSubmitStatus = next.id == submitStatusId;
|
final isSubmitStatus = next.id == submitStatusId;
|
||||||
final isCreatedByCurrentUser =
|
final isCreatedByCurrentUser = controller.employeeInfo?.id ==
|
||||||
controller.employeeInfo?.id == expense.createdBy.id; // Use controller's employeeInfo
|
expense.createdBy.id;
|
||||||
|
|
||||||
if (isSubmitStatus) return isCreatedByCurrentUser;
|
if (isSubmitStatus) return isCreatedByCurrentUser;
|
||||||
return permissionController.hasAnyPermission(parsedPermissions);
|
return permissionController.hasAnyPermission(parsedPermissions);
|
||||||
}).map((next) {
|
}).map((next) {
|
||||||
@ -311,8 +311,7 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
|||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius:
|
borderRadius: BorderRadius.vertical(top: Radius.circular(5))),
|
||||||
BorderRadius.vertical(top: Radius.circular(5))),
|
|
||||||
builder: (context) => ReimbursementBottomSheet(
|
builder: (context) => ReimbursementBottomSheet(
|
||||||
expenseId: expense.id,
|
expenseId: expense.id,
|
||||||
statusId: next.id,
|
statusId: next.id,
|
||||||
@ -785,4 +784,4 @@ class _InvoiceTotals extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,6 +95,7 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
|
|||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: "Expense & Reimbursement",
|
title: "Expense & Reimbursement",
|
||||||
|
projectName: " All Projects",
|
||||||
backgroundColor: appBarColor,
|
backgroundColor: appBarColor,
|
||||||
onBackPressed: () => Get.toNamed('/dashboard/finance'),
|
onBackPressed: () => Get.toNamed('/dashboard/finance'),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -54,6 +54,7 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
|
|||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: "Advance Payments",
|
title: "Advance Payments",
|
||||||
|
projectName: " All Projects",
|
||||||
onBackPressed: () => Get.offNamed('/dashboard/finance'),
|
onBackPressed: () => Get.offNamed('/dashboard/finance'),
|
||||||
backgroundColor: appBarColor,
|
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/model/dynamicMenu/dynamic_menu_model.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||||
|
|
||||||
|
|
||||||
class FinanceScreen extends StatefulWidget {
|
class FinanceScreen extends StatefulWidget {
|
||||||
const FinanceScreen({super.key});
|
const FinanceScreen({super.key});
|
||||||
|
|
||||||
@ -59,7 +58,8 @@ class _FinanceScreenState extends State<FinanceScreen>
|
|||||||
backgroundColor: const Color(0xFFF8F9FA),
|
backgroundColor: const Color(0xFFF8F9FA),
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: "Finance",
|
title: "Finance",
|
||||||
onBackPressed: () => Get.offAllNamed( '/dashboard' ),
|
projectName: " All Projects",
|
||||||
|
onBackPressed: () => Get.offAllNamed('/dashboard'),
|
||||||
backgroundColor: appBarColor,
|
backgroundColor: appBarColor,
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
|
|||||||
@ -114,6 +114,7 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
|
|||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: "Payment Request Details",
|
title: "Payment Request Details",
|
||||||
|
projectName: " All Projects",
|
||||||
backgroundColor: appBarColor,
|
backgroundColor: appBarColor,
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
@ -217,7 +218,7 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_checkedPermission && employeeInfo != null) {
|
if (!_checkedPermission && employeeInfo != null) {
|
||||||
_checkedPermission = true;
|
_checkedPermission = true;
|
||||||
_checkPermissionToSubmit(request);
|
_checkPermissionToSubmit(request);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,6 +104,7 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
|||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: "Payment Requests",
|
title: "Payment Requests",
|
||||||
|
projectName: " All Projects",
|
||||||
onBackPressed: () => Get.offNamed('/dashboard/finance'),
|
onBackPressed: () => Get.offNamed('/dashboard/finance'),
|
||||||
backgroundColor: appBarColor,
|
backgroundColor: appBarColor,
|
||||||
),
|
),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user