feat: Enhance Dashboard with Attendance and Infra Projects

- Added employee attendance fetching in DashboardController.
- Introduced loading state for employees in the dashboard.
- Updated API endpoints to include attendance for the dashboard.
- Created a new InfraProjectsMainScreen with tab navigation for task planning and progress reporting.
- Improved UI components for better user experience in the dashboard.
- Refactored project selection and quick actions in the dashboard.
- Added permission constants for infrastructure projects.
This commit is contained in:
Vaibhav Surve 2025-12-03 13:09:48 +05:30
parent cf85c17d75
commit 03e3e7b5db
9 changed files with 877 additions and 408 deletions

View File

@ -1,37 +1,41 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
import 'package:on_field_work/model/attendance/attendance_model.dart';
import 'package:on_field_work/model/project_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/attendance/attendance_log_model.dart';
import 'package:on_field_work/model/regularization_log_model.dart';
import 'package:on_field_work/model/attendance/attendance_log_view_model.dart';
import 'package:on_field_work/model/attendance/attendance_model.dart';
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/project_model.dart';
import 'package:on_field_work/model/regularization_log_model.dart';
class AttendanceController extends GetxController {
// ------------------ Data Models ------------------
List<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
List<EmployeeModel> employees = [];
List<AttendanceLogModel> attendanceLogs = [];
List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = [];
final List<AttendanceModel> attendances = <AttendanceModel>[];
final List<ProjectModel> projects = <ProjectModel>[];
final List<EmployeeModel> employees = <EmployeeModel>[];
final List<AttendanceLogModel> attendanceLogs = <AttendanceLogModel>[];
final List<RegularizationLogModel> regularizationLogs =
<RegularizationLogModel>[];
final List<AttendanceLogViewModel> attendenceLogsView =
<AttendanceLogViewModel>[];
// ------------------ Organizations ------------------
List<Organization> organizations = [];
final List<Organization> organizations = <Organization>[];
Organization? selectedOrganization;
final isLoadingOrganizations = false.obs;
final RxBool isLoadingOrganizations = false.obs;
// ------------------ States ------------------
String selectedTab = 'todaysAttendance';
@ -42,16 +46,17 @@ class AttendanceController extends GetxController {
final Rx<DateTime> endDateAttendance =
DateTime.now().subtract(const Duration(days: 1)).obs;
final isLoading = true.obs;
final isLoadingProjects = true.obs;
final isLoadingEmployees = true.obs;
final isLoadingAttendanceLogs = true.obs;
final isLoadingRegularizationLogs = true.obs;
final isLoadingLogView = true.obs;
final uploadingStates = <String, RxBool>{}.obs;
var showPendingOnly = false.obs;
final RxBool isLoading = true.obs;
final RxBool isLoadingProjects = true.obs;
final RxBool isLoadingEmployees = true.obs;
final RxBool isLoadingAttendanceLogs = true.obs;
final RxBool isLoadingRegularizationLogs = true.obs;
final RxBool isLoadingLogView = true.obs;
final searchQuery = ''.obs;
final RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
final RxBool showPendingOnly = false.obs;
final RxString searchQuery = ''.obs;
@override
void onInit() {
@ -64,35 +69,43 @@ class AttendanceController extends GetxController {
}
void _setDefaultDateRange() {
final today = DateTime.now();
final DateTime today = DateTime.now();
startDateAttendance.value = today.subtract(const Duration(days: 7));
endDateAttendance.value = today.subtract(const Duration(days: 1));
logSafe(
"Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}");
'Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}',
);
}
// ------------------ Computed Filters ------------------
List<EmployeeModel> get filteredEmployees {
if (searchQuery.value.isEmpty) return employees;
final String query = searchQuery.value.trim().toLowerCase();
if (query.isEmpty) return employees;
return employees
.where((e) =>
e.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.where(
(EmployeeModel e) => e.name.toLowerCase().contains(query),
)
.toList();
}
List<AttendanceLogModel> get filteredLogs {
if (searchQuery.value.isEmpty) return attendanceLogs;
final String query = searchQuery.value.trim().toLowerCase();
if (query.isEmpty) return attendanceLogs;
return attendanceLogs
.where((log) =>
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.where(
(AttendanceLogModel log) => log.name.toLowerCase().contains(query),
)
.toList();
}
List<RegularizationLogModel> get filteredRegularizationLogs {
if (searchQuery.value.isEmpty) return regularizationLogs;
final String query = searchQuery.value.trim().toLowerCase();
if (query.isEmpty) return regularizationLogs;
return regularizationLogs
.where((log) =>
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.where(
(RegularizationLogModel log) =>
log.name.toLowerCase().contains(query),
)
.toList();
}
@ -100,13 +113,16 @@ class AttendanceController extends GetxController {
Future<void> refreshDataFromNotification({String? projectId}) async {
projectId ??= Get.find<ProjectController>().selectedProject?.id;
if (projectId == null) {
logSafe("No project selected for attendance refresh from notification",
level: LogLevel.warning);
logSafe(
'No project selected for attendance refresh from notification',
level: LogLevel.warning,
);
return;
}
await fetchProjectData(projectId);
logSafe(
"Attendance data refreshed from notification for project $projectId");
'Attendance data refreshed from notification for project $projectId',
);
}
Future<void> fetchTodaysAttendance(String? projectId) async {
@ -114,19 +130,35 @@ class AttendanceController extends GetxController {
isLoadingEmployees.value = true;
final response = await ApiService.getTodaysAttendance(
final List<dynamic>? response = await ApiService.getTodaysAttendance(
projectId,
organizationId: selectedOrganization?.id,
);
if (response != null) {
employees = response.map((e) => EmployeeModel.fromJson(e)).toList();
for (var emp in employees) {
employees
..clear()
..addAll(
response
.map<EmployeeModel>(
(dynamic e) => EmployeeModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
for (final EmployeeModel emp in employees) {
uploadingStates[emp.id] = false.obs;
}
logSafe("Employees fetched: ${employees.length} for project $projectId");
logSafe(
'Employees fetched: ${employees.length} for project $projectId',
);
} else {
logSafe("Failed to fetch employees for project $projectId",
level: LogLevel.error);
logSafe(
'Failed to fetch employees for project $projectId',
level: LogLevel.error,
);
}
isLoadingEmployees.value = false;
@ -135,14 +167,22 @@ class AttendanceController extends GetxController {
Future<void> fetchOrganizations(String projectId) async {
isLoadingOrganizations.value = true;
// Keep original return type inference from your ApiService
final response = await ApiService.getAssignedOrganizations(projectId);
if (response != null) {
organizations = response.data;
logSafe("Organizations fetched: ${organizations.length}");
organizations
..clear()
..addAll(response.data);
logSafe('Organizations fetched: ${organizations.length}');
} else {
logSafe("Failed to fetch organizations for project $projectId",
level: LogLevel.error);
logSafe(
'Failed to fetch organizations for project $projectId',
level: LogLevel.error,
);
}
isLoadingOrganizations.value = false;
update();
}
@ -152,61 +192,43 @@ class AttendanceController extends GetxController {
String id,
String employeeId,
String projectId, {
String comment = "Marked via mobile app",
String comment = 'Marked via mobile app',
required int action,
bool imageCapture = true,
String? markTime,
String? date,
}) async {
try {
uploadingStates[employeeId]?.value = true;
_setUploading(employeeId, true);
XFile? image;
if (imageCapture) {
image = await ImagePicker()
.pickImage(source: ImageSource.camera, imageQuality: 80);
if (image == null) {
logSafe("Image capture cancelled.", level: LogLevel.warning);
return false;
}
final timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: File(image.path));
final compressedBytes =
await compressImageToUnder100KB(timestampedFile);
if (compressedBytes == null) {
logSafe("Image compression failed.", level: LogLevel.error);
return false;
}
final compressedFile = await saveCompressedImageToFile(compressedBytes);
image = XFile(compressedFile.path);
final XFile? image = await _captureAndPrepareImage(
employeeId: employeeId,
imageCapture: imageCapture,
);
if (imageCapture && image == null) {
return false;
}
if (!await _handleLocationPermission()) return false;
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
final Position? position = await _getCurrentPositionSafely();
if (position == null) return false;
final imageName = imageCapture
? ApiService.generateImageName(employeeId, employees.length + 1)
: "";
final String imageName = imageCapture
? ApiService.generateImageName(
employeeId,
employees.length + 1,
)
: '';
final now = DateTime.now();
DateTime effectiveDate = now;
final DateTime effectiveDate =
_resolveEffectiveDateForAction(action, employeeId);
if (action == 1) {
final log = attendanceLogs.firstWhereOrNull(
(log) => log.employeeId == employeeId && log.checkOut == null,
);
if (log?.checkIn != null) effectiveDate = log!.checkIn!;
}
final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now);
final formattedDate =
final DateTime now = DateTime.now();
final String formattedMarkTime =
markTime ?? DateFormat('hh:mm a').format(now);
final String formattedDate =
date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
final result = await ApiService.uploadAttendanceImage(
final bool result = await ApiService.uploadAttendanceImage(
id,
employeeId,
image,
@ -221,15 +243,99 @@ class AttendanceController extends GetxController {
date: formattedDate,
);
logSafe(
"Attendance uploaded for $employeeId, action: $action, date: $formattedDate");
if (result) {
logSafe(
'Attendance uploaded for $employeeId, action: $action, date: $formattedDate',
);
if (Get.isRegistered<DashboardController>()) {
final DashboardController dashboardController =
Get.find<DashboardController>();
await dashboardController.fetchTodaysAttendance(projectId);
}
}
return result;
} catch (e, stacktrace) {
logSafe("Error uploading attendance",
level: LogLevel.error, error: e, stackTrace: stacktrace);
logSafe(
'Error uploading attendance',
level: LogLevel.error,
error: e,
stackTrace: stacktrace,
);
return false;
} finally {
uploadingStates[employeeId]?.value = false;
_setUploading(employeeId, false);
}
}
Future<XFile?> _captureAndPrepareImage({
required String employeeId,
required bool imageCapture,
}) async {
if (!imageCapture) return null;
final XFile? rawImage = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (rawImage == null) {
logSafe(
'Image capture cancelled.',
level: LogLevel.warning,
);
return null;
}
final File timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: File(rawImage.path),
);
final List<int>? compressedBytes =
await compressImageToUnder100KB(timestampedFile);
if (compressedBytes == null) {
logSafe(
'Image compression failed.',
level: LogLevel.error,
);
return null;
}
// FIX: convert List<int> -> Uint8List
final Uint8List compressedUint8List = Uint8List.fromList(compressedBytes);
final File compressedFile =
await saveCompressedImageToFile(compressedUint8List);
return XFile(compressedFile.path);
}
Future<Position?> _getCurrentPositionSafely() async {
final bool permissionGranted = await _handleLocationPermission();
if (!permissionGranted) return null;
return Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
}
DateTime _resolveEffectiveDateForAction(int action, String employeeId) {
final DateTime now = DateTime.now();
if (action != 1) return now;
final AttendanceLogModel? log = attendanceLogs.firstWhereOrNull(
(AttendanceLogModel log) =>
log.employeeId == employeeId && log.checkOut == null,
);
return log?.checkIn ?? now;
}
void _setUploading(String employeeId, bool value) {
final RxBool? state = uploadingStates[employeeId];
if (state != null) {
state.value = value;
} else {
uploadingStates[employeeId] = value.obs;
}
}
@ -239,14 +345,19 @@ class AttendanceController extends GetxController {
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
logSafe('Location permissions are denied', level: LogLevel.warning);
logSafe(
'Location permissions are denied',
level: LogLevel.warning,
);
return false;
}
}
if (permission == LocationPermission.deniedForever) {
logSafe('Location permissions are permanently denied',
level: LogLevel.error);
logSafe(
'Location permissions are permanently denied',
level: LogLevel.error,
);
return false;
}
@ -254,25 +365,40 @@ class AttendanceController extends GetxController {
}
// ------------------ Attendance Logs ------------------
Future<void> fetchAttendanceLogs(String? projectId,
{DateTime? dateFrom, DateTime? dateTo}) async {
Future<void> fetchAttendanceLogs(
String? projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return;
isLoadingAttendanceLogs.value = true;
final response = await ApiService.getAttendanceLogs(
final List<dynamic>? response = await ApiService.getAttendanceLogs(
projectId,
dateFrom: dateFrom,
dateTo: dateTo,
organizationId: selectedOrganization?.id,
);
if (response != null) {
attendanceLogs =
response.map((e) => AttendanceLogModel.fromJson(e)).toList();
logSafe("Attendance logs fetched: ${attendanceLogs.length}");
attendanceLogs
..clear()
..addAll(
response
.map<AttendanceLogModel>(
(dynamic e) => AttendanceLogModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
logSafe('Attendance logs fetched: ${attendanceLogs.length}');
} else {
logSafe("Failed to fetch attendance logs for project $projectId",
level: LogLevel.error);
logSafe(
'Failed to fetch attendance logs for project $projectId',
level: LogLevel.error,
);
}
isLoadingAttendanceLogs.value = false;
@ -280,25 +406,37 @@ class AttendanceController extends GetxController {
}
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final groupedLogs = <String, List<AttendanceLogModel>>{};
final Map<String, List<AttendanceLogModel>> groupedLogs =
<String, List<AttendanceLogModel>>{};
for (var logItem in attendanceLogs) {
final checkInDate = logItem.checkIn != null
for (final AttendanceLogModel logItem in attendanceLogs) {
final String checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown';
groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem);
groupedLogs.putIfAbsent(
checkInDate,
() => <AttendanceLogModel>[],
)..add(logItem);
}
final sortedEntries = groupedLogs.entries.toList()
..sort((a, b) {
if (a.key == 'Unknown') return 1;
if (b.key == 'Unknown') return -1;
final dateA = DateFormat('dd MMM yyyy').parse(a.key);
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA);
});
final List<MapEntry<String, List<AttendanceLogModel>>> sortedEntries =
groupedLogs.entries.toList()
..sort(
(MapEntry<String, List<AttendanceLogModel>> a,
MapEntry<String, List<AttendanceLogModel>> b) {
if (a.key == 'Unknown') return 1;
if (b.key == 'Unknown') return -1;
return Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
final DateTime dateA = DateFormat('dd MMM yyyy').parse(a.key);
final DateTime dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA);
},
);
return Map<String, List<AttendanceLogModel>>.fromEntries(
sortedEntries,
);
}
// ------------------ Regularization Logs ------------------
@ -307,17 +445,31 @@ class AttendanceController extends GetxController {
isLoadingRegularizationLogs.value = true;
final response = await ApiService.getRegularizationLogs(
final List<dynamic>? response = await ApiService.getRegularizationLogs(
projectId,
organizationId: selectedOrganization?.id,
);
if (response != null) {
regularizationLogs =
response.map((e) => RegularizationLogModel.fromJson(e)).toList();
logSafe("Regularization logs fetched: ${regularizationLogs.length}");
regularizationLogs
..clear()
..addAll(
response
.map<RegularizationLogModel>(
(dynamic e) => RegularizationLogModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
logSafe(
'Regularization logs fetched: ${regularizationLogs.length}',
);
} else {
logSafe("Failed to fetch regularization logs for project $projectId",
level: LogLevel.error);
logSafe(
'Failed to fetch regularization logs for project $projectId',
level: LogLevel.error,
);
}
isLoadingRegularizationLogs.value = false;
@ -330,16 +482,33 @@ class AttendanceController extends GetxController {
isLoadingLogView.value = true;
final response = await ApiService.getAttendanceLogView(id);
final List<dynamic>? response = await ApiService.getAttendanceLogView(id);
if (response != null) {
attendenceLogsView =
response.map((e) => AttendanceLogViewModel.fromJson(e)).toList();
attendenceLogsView.sort((a, b) => (b.activityTime ?? DateTime(2000))
.compareTo(a.activityTime ?? DateTime(2000)));
logSafe("Attendance log view fetched for ID: $id");
attendenceLogsView
..clear()
..addAll(
response
.map<AttendanceLogViewModel>(
(dynamic e) => AttendanceLogViewModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
attendenceLogsView.sort(
(AttendanceLogViewModel a, AttendanceLogViewModel b) =>
(b.activityTime ?? DateTime(2000))
.compareTo(a.activityTime ?? DateTime(2000)),
);
logSafe('Attendance log view fetched for ID: $id');
} else {
logSafe("Failed to fetch attendance log view for ID $id",
level: LogLevel.error);
logSafe(
'Failed to fetch attendance log view for ID $id',
level: LogLevel.error,
);
}
isLoadingLogView.value = false;
@ -375,16 +544,19 @@ class AttendanceController extends GetxController {
}
logSafe(
"Project data fetched for project ID: $projectId, tab: $selectedTab");
update();
'Project data fetched for project ID: $projectId, tab: $selectedTab',
);
update();
}
// ------------------ UI Interaction ------------------
Future<void> selectDateRangeForAttendance(
BuildContext context, AttendanceController controller) async {
final today = DateTime.now();
BuildContext context,
AttendanceController controller,
) async {
final DateTime today = DateTime.now();
final picked = await showDateRangePicker(
final DateTimeRange? picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2022),
lastDate: today.subtract(const Duration(days: 1)),
@ -399,7 +571,8 @@ class AttendanceController extends GetxController {
endDateAttendance.value = picked.end;
logSafe(
"Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}");
'Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}',
);
await controller.fetchAttendanceLogs(
Get.find<ProjectController>().selectedProject?.id,

View File

@ -7,6 +7,7 @@ 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';
class DashboardController extends GetxController {
// =========================
@ -81,6 +82,10 @@ class DashboardController extends GetxController {
final RxInt selectedMonthsCount = 12.obs;
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
final Rx<ExpenseTypeModel?> selectedExpenseType = Rx<ExpenseTypeModel?>(null);
final isLoadingEmployees = true.obs;
// DashboardController
final RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
final uploadingStates = <String, RxBool>{}.obs;
void updateSelectedExpenseType(ExpenseTypeModel? type) {
selectedExpenseType.value = type;
@ -100,24 +105,35 @@ class DashboardController extends GetxController {
super.onInit();
logSafe(
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
'DashboardController initialized',
level: LogLevel.info,
);
fetchAllDashboardData();
// React to project change
// React to project selection
ever<String>(projectController.selectedProjectId, (id) {
fetchAllDashboardData();
if (id.isNotEmpty) {
logSafe('Project selected: $id', level: LogLevel.info);
fetchAllDashboardData();
fetchTodaysAttendance(id);
} else {
logSafe('No project selected yet.', level: LogLevel.warning);
}
});
// React to expense report date changes
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
fetchExpenseTypeReport(
startDate: expenseReportStartDate.value,
endDate: expenseReportEndDate.value,
);
if (projectController.selectedProjectId.value.isNotEmpty) {
fetchExpenseTypeReport(
startDate: expenseReportStartDate.value,
endDate: expenseReportEndDate.value,
);
}
});
// React to range changes
// React to attendance range changes
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
// React to project range changes
ever(projectSelectedRange, (_) => fetchProjectProgress());
}
@ -208,6 +224,26 @@ class DashboardController extends GetxController {
]);
}
Future<void> fetchTodaysAttendance(String projectId) async {
isLoadingEmployees.value = true;
final response = await ApiService.getAttendanceForDashboard(projectId);
if (response != null) {
employees.value = response;
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
logSafe(
"Dashboard Attendance fetched: ${employees.length} for project $projectId");
} else {
logSafe("Failed to fetch Dashboard Attendance for project $projectId",
level: LogLevel.error);
}
isLoadingEmployees.value = false;
update();
}
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
selectedMonthlyExpenseDuration.value = duration;
@ -359,7 +395,8 @@ class DashboardController extends GetxController {
level: LogLevel.info);
} else {
expenseTypeReportData.value = null;
logSafe('Failed to fetch Expense Category Report.', level: LogLevel.error);
logSafe('Failed to fetch Expense Category Report.',
level: LogLevel.error);
}
} catch (e, st) {
expenseTypeReportData.value = null;

View File

@ -1,9 +1,9 @@
class ApiEndpoints {
// static const String baseUrl = "https://stageapi.marcoaiot.com/api";
static const String baseUrl = "https://stageapi.marcoaiot.com/api";
// static const String baseUrl = "https://api.marcoaiot.com/api";
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
static const String baseUrl = "https://api.onfieldwork.com/api";
// static const String baseUrl = "https://api.onfieldwork.com/api";
static const String getMasterCurrencies = "/Master/currencies/list";
@ -44,6 +44,7 @@ class ApiEndpoints {
static const String getProjects = "/project/list";
static const String getGlobalProjects = "/project/list/basic";
static const String getTodaysAttendance = "/attendance/project/team";
static const String getAttendanceForDashboard = "/dashboard/get/attendance/employee/:projectId";
static const String getAttendanceLogs = "/attendance/project/log";
static const String getAttendanceLogView = "/attendance/log/attendance";
static const String getRegularizationLogs = "/attendance/regularize";

View File

@ -42,6 +42,7 @@ import 'package:on_field_work/model/service_project/job_allocation_model.dart';
import 'package:on_field_work/model/service_project/service_project_branches_model.dart';
import 'package:on_field_work/model/service_project/job_status_response.dart';
import 'package:on_field_work/model/service_project/job_comments.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
class ApiService {
static const bool enableLogs = true;
@ -3320,6 +3321,30 @@ class ApiService {
res != null ? _parseResponse(res, label: 'Employees') : null);
}
static Future<List<EmployeeModel>?> getAttendanceForDashboard(
String projectId) async {
String endpoint = ApiEndpoints.getAttendanceForDashboard.replaceFirst(
':projectId',
projectId,
);
final res = await _getRequest(endpoint);
if (res == null) return null;
final data = _parseResponse(res, label: 'Dashboard Attendance');
if (data == null) return null;
// Wrap single object in a list if needed
if (data is Map<String, dynamic>) {
return [EmployeeModel.fromJson(data)];
} else if (data is List) {
return data.map((e) => EmployeeModel.fromJson(e)).toList();
}
return null;
}
static Future<List<dynamic>?> getRegularizationLogs(
String projectId, {
String? organizationId,

View File

@ -163,6 +163,9 @@ class MenuItems {
/// Service Projects
static const String serviceProjects = "7faddfe7-994b-4712-91c2-32ba44129d9b";
/// Infrastructure Projects
static const String infraProjects = "d3b5f3e3-3f7c-4f2b-99f1-1c9e4b8e6c2a";
}
/// Contains all job status IDs used across the application.

View File

@ -271,13 +271,9 @@ class AttendanceActionButtonUI extends StatelessWidget {
textStyle: const TextStyle(fontSize: 12),
),
child: isUploading
? Container(
width: 60,
height: 14,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
? const Text(
'Loading...',
style: TextStyle(fontSize: 12, color: Colors.white),
)
: Row(
mainAxisSize: MainAxisSize.min,

View File

@ -38,11 +38,12 @@ class _AttendanceScreenState extends State<AttendanceScreen>
void initState() {
super.initState();
// Watch permissions loaded
ever(permissionController.permissionsLoaded, (loaded) {
if (loaded == true && !_tabsInitialized) {
_initializeTabs();
setState(() {});
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializeTabs();
setState(() {});
});
}
});

View File

@ -1,18 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:get/get.dart';
import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart';
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/avatar.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/model/attendance/attendence_action_button.dart';
import 'package:on_field_work/model/attendance/log_details_view.dart';
import 'package:on_field_work/view/layouts/layout.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart ';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@ -24,7 +29,10 @@ class DashboardScreen extends StatefulWidget {
class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
final DashboardController dashboardController =
Get.put(DashboardController(), permanent: true);
final DynamicMenuController menuController = Get.put(DynamicMenuController());
final AttendanceController attendanceController =
Get.put(AttendanceController());
final DynamicMenuController menuController =
Get.put(DynamicMenuController());
final ProjectController projectController = Get.find<ProjectController>();
bool hasMpin = true;
@ -37,12 +45,14 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
Future<void> _checkMpinStatus() async {
hasMpin = await LocalStorage.getIsMpin();
if (mounted) setState(() {});
if (mounted) {
setState(() {});
}
}
//---------------------------------------------------------------------------
// REUSABLE CARD (smaller, minimal)
//---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
Widget _cardWrapper({required Widget child}) {
return Container(
@ -56,17 +66,13 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
color: Colors.black12.withOpacity(.05),
blurRadius: 12,
offset: const Offset(0, 4),
)
),
],
),
child: child,
);
}
//---------------------------------------------------------------------------
// SECTION TITLE
//---------------------------------------------------------------------------
Widget _sectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
@ -81,189 +87,150 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
);
}
Widget _conditionalQuickActionCard() {
String status = "1"; // <-- change as needed
bool isCheckedIn = status == "O";
// Button color remains the same
Color buttonColor =
isCheckedIn ? Colors.red.shade700 : Colors.green.shade700;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
gradient: LinearGradient(
colors: [
contentTheme.primary.withOpacity(0.3), // lighter/faded
contentTheme.primary.withOpacity(0.6), // slightly stronger
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.05),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title & Status
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isCheckedIn ? "Checked-In" : "Not Checked-In",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Icon(
isCheckedIn ? LucideIcons.log_out : LucideIcons.log_in,
color: Colors.white,
size: 24,
),
],
),
const SizedBox(height: 8),
// Description
Text(
isCheckedIn
? "You are currently checked-in. Don't forget to check-out after your work."
: "You are not checked-in yet. Please check-in to start your work.",
style: const TextStyle(color: Colors.white70, fontSize: 13),
),
const SizedBox(height: 12),
// Action Button (solid color)
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton.icon(
onPressed: () {
// Check-In / Check-Out action
},
icon: Icon(
isCheckedIn ? LucideIcons.log_out : LucideIcons.log_in,
size: 16,
),
label: Text(isCheckedIn ? "Check-Out" : "Check-In"),
style: ElevatedButton.styleFrom(
backgroundColor: buttonColor,
foregroundColor: Colors.white,
),
),
],
),
],
),
);
}
//---------------------------------------------------------------------------
// QUICK ACTIONS (updated to use the single card)
//---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Quick Actions
// ---------------------------------------------------------------------------
Widget _quickActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle("Quick Action"),
_conditionalQuickActionCard(),
_sectionTitle('Quick Action'),
Obx(() {
final employees = dashboardController.employees;
final employee = employees.isNotEmpty ? employees.first : null;
if (employee == null) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
gradient: LinearGradient(
colors: [
contentTheme.primary.withOpacity(0.3),
contentTheme.primary.withOpacity(0.6),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: const Text(
'No attendance data available',
style: TextStyle(color: Colors.white),
),
);
}
final bool isCheckedIn = employee.checkIn != null;
final bool isCheckedOut = employee.checkOut != null;
final String statusText = !isCheckedIn
? 'Check In Pending'
: isCheckedIn && !isCheckedOut
? 'Checked In'
: 'Checked Out';
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
gradient: LinearGradient(
colors: [
contentTheme.primary.withOpacity(0.3),
contentTheme.primary.withOpacity(0.6),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 30,
),
MySpacing.width(10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleSmall(
employee.name,
fontWeight: 600,
color: Colors.white,
),
MyText.labelSmall(
employee.designation,
fontWeight: 500,
color: Colors.white70,
),
],
),
),
MyText.bodySmall(
statusText,
fontWeight: 600,
color: Colors.white,
),
],
),
const SizedBox(height: 12),
Text(
!isCheckedIn
? 'You are not checked-in yet. Please check-in to start your work.'
: !isCheckedOut
? 'You are currently checked-in. Don\'t forget to check-out after your work.'
: 'You have checked-out for today.',
style: const TextStyle(
color: Colors.white70,
fontSize: 13,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AttendanceActionButton(
employee: employee,
attendanceController: attendanceController,
),
if (isCheckedIn) ...[
MySpacing.width(8),
AttendanceLogViewButton(
employee: employee,
attendanceController: attendanceController,
),
],
],
),
],
),
);
}),
],
);
}
//---------------------------------------------------------------------------
// PROJECT DROPDOWN (clean compact)
//---------------------------------------------------------------------------
Widget _projectSelector() {
return Obx(() {
final isLoading = projectController.isLoading.value;
final expanded = projectController.isProjectSelectionExpanded.value;
final projects = projectController.projects;
final selectedId = projectController.selectedProjectId.value;
// ---------------------------------------------------------------------------
// Dashboard Modules
// ---------------------------------------------------------------------------
if (isLoading) {
// Use skeleton instead of CircularProgressIndicator
return SkeletonLoaders.dashboardCardsSkeleton(
maxWidth: MediaQuery.of(context).size.width);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle("Project"),
// Compact Selector
GestureDetector(
onTap: () => projectController.isProjectSelectionExpanded.toggle(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.black12.withOpacity(.15)),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(.04),
blurRadius: 6,
offset: const Offset(0, 2),
)
],
),
child: Row(
children: [
const Icon(Icons.work_outline, color: Colors.blue, size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
projects
.firstWhereOrNull((p) => p.id == selectedId)
?.name ??
"Select Project",
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600),
),
),
Icon(
expanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 26,
color: Colors.black54,
)
],
),
),
),
if (expanded) _projectDropdownList(projects, selectedId),
],
);
});
}
//---------------------------------------------------------------------------
// DASHBOARD MODULE CARDS (UPDATED FOR MINIMAL PADDING / SLL SIZE)
//---------------------------------------------------------------------------
Widget _dashboardCards() {
Widget _dashboardModules() {
return Obx(() {
if (menuController.isLoading.value) {
// Show skeleton instead of CircularProgressIndicator
return SkeletonLoaders.dashboardCardsSkeleton(
maxWidth: MediaQuery.of(context).size.width);
maxWidth: MediaQuery.of(context).size.width,
);
}
final projectSelected = projectController.selectedProject != null;
final bool projectSelected = projectController.selectedProject != null;
final cardOrder = [
// these are String constants from permission_constants.dart
final List<String> cardOrder = [
MenuItems.attendance,
MenuItems.employees,
MenuItems.dailyTaskPlanning,
@ -272,9 +239,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
MenuItems.finance,
MenuItems.documents,
MenuItems.serviceProjects,
MenuItems.infraProjects,
];
final meta = {
final Map<String, _DashboardCardMeta> meta = {
MenuItems.attendance:
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
MenuItems.employees:
@ -286,97 +254,142 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
MenuItems.directory:
_DashboardCardMeta(LucideIcons.folder, contentTheme.info),
MenuItems.finance:
_DashboardCardMeta(LucideIcons.wallet, contentTheme.info),
_DashboardCardMeta(LucideIcons.wallet, contentTheme.info),
MenuItems.documents:
_DashboardCardMeta(LucideIcons.file_text, contentTheme.info),
MenuItems.serviceProjects:
_DashboardCardMeta(LucideIcons.package, contentTheme.info),
MenuItems.infraProjects:
_DashboardCardMeta(LucideIcons.building_2, contentTheme.primary),
};
final allowed = {
for (var m in menuController.menuItems)
if (m.available && meta.containsKey(m.id)) m.id: m
final Map<String, dynamic> allowed = {
for (final m in menuController.menuItems)
if (m.available && meta.containsKey(m.id)) m.id: m,
};
final filtered = cardOrder.where(allowed.containsKey).toList();
final List<String> filtered =
cardOrder.where((id) => allowed.containsKey(id)).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle("Modules"),
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Modules',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.black87,
),
),
if (!projectSelected)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'Select Project',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
),
],
),
),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
// **More compact grid**
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 6,
mainAxisSpacing: 6,
childAspectRatio: 1.2, // smaller & tighter
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.15,
),
itemCount: filtered.length,
itemBuilder: (context, index) {
final id = filtered[index];
final String id = filtered[index];
final item = allowed[id]!;
final cardMeta = meta[id]!;
final _DashboardCardMeta cardMeta = meta[id]!;
final isEnabled =
item.name == "Attendance" ? true : projectSelected;
final bool isEnabled =
item.name == 'Attendance' ? true : projectSelected;
return GestureDetector(
onTap: () {
if (!isEnabled) {
Get.defaultDialog(
title: "No Project Selected",
middleText: "Please select a project first.",
Get.snackbar(
'Required',
'Please select a project first',
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(16),
backgroundColor: Colors.black87,
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
} else {
Get.toNamed(item.mobileLink);
}
},
child: Container(
// **Reduced padding**
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isEnabled ? Colors.white : Colors.grey.shade100,
borderRadius: BorderRadius.circular(5),
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.black12.withOpacity(.1),
width: 0.7,
color: isEnabled
? Colors.black12.withOpacity(0.06)
: Colors.transparent,
),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(.05),
color: Colors.black.withOpacity(0.03),
blurRadius: 4,
offset: const Offset(0, 2),
)
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
cardMeta.icon,
size: 20,
color:
isEnabled ? cardMeta.color : Colors.grey.shade400,
color: isEnabled
? cardMeta.color
: Colors.grey.shade300,
),
const SizedBox(height: 3),
Text(
item.name,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 9.5,
fontWeight: FontWeight.w600,
color:
isEnabled ? Colors.black87 : Colors.grey.shade600,
const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Text(
item.name,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 10,
fontWeight: isEnabled
? FontWeight.w600
: FontWeight.w400,
color: isEnabled
? Colors.black87
: Colors.grey.shade400,
height: 1.2,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
@ -389,7 +402,85 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
});
}
Widget _projectDropdownList(projects, selectedId) {
// ---------------------------------------------------------------------------
// Project Selector
// ---------------------------------------------------------------------------
Widget _projectSelector() {
return Obx(() {
final bool isLoading = projectController.isLoading.value;
final bool expanded = projectController.isProjectSelectionExpanded.value;
final projects = projectController.projects;
final String? selectedId = projectController.selectedProjectId.value;
if (isLoading) {
return SkeletonLoaders.dashboardCardsSkeleton(
maxWidth: MediaQuery.of(context).size.width,
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Project'),
GestureDetector(
onTap: () =>
projectController.isProjectSelectionExpanded.toggle(),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.black12.withOpacity(.15)),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(.04),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
const Icon(
Icons.work_outline,
color: Colors.blue,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
projects
.firstWhereOrNull(
(p) => p.id == selectedId,
)
?.name ??
'Select Project',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
Icon(
expanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 26,
color: Colors.black54,
),
],
),
),
),
if (expanded) _projectDropdownList(projects, selectedId),
],
);
});
}
Widget _projectDropdownList(List projects, String? selectedId) {
return Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.all(12),
@ -405,17 +496,19 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
),
],
),
constraints:
BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.33),
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.33,
),
child: Column(
children: [
TextField(
decoration: InputDecoration(
hintText: "Search project...",
hintText: 'Search project...',
isDense: true,
prefixIcon: const Icon(Icons.search),
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(5)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
),
),
),
const SizedBox(height: 10),
@ -428,9 +521,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
dense: true,
value: project.id,
groupValue: selectedId,
onChanged: (v) {
if (v != null) {
projectController.updateSelectedProject(v);
onChanged: (value) {
if (value != null) {
projectController.updateSelectedProject(value);
projectController.isProjectSelectionExpanded.value =
false;
}
@ -445,9 +538,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
);
}
//---------------------------------------------------------------------------
// MAIN UI
//---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Build
// ---------------------------------------------------------------------------
@override
Widget build(BuildContext context) {
@ -460,18 +553,23 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_projectSelector(),
// MySpacing.height(20),
// _quickActions(),
MySpacing.height(20),
// The updated module cards
_dashboardCards(),
_quickActions(),
MySpacing.height(20),
_sectionTitle("Reports & Analytics"),
_cardWrapper(child: ExpenseTypeReportChart()),
_dashboardModules(),
MySpacing.height(20),
_sectionTitle('Reports & Analytics'),
_cardWrapper(
child:
ExpenseByStatusWidget(controller: dashboardController)),
_cardWrapper(child: MonthlyExpenseDashboardChart()),
child: ExpenseTypeReportChart(),
),
_cardWrapper(
child: ExpenseByStatusWidget(
controller: dashboardController,
),
),
_cardWrapper(
child: MonthlyExpenseDashboardChart(),
),
MySpacing.height(20),
],
),
@ -484,5 +582,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
class _DashboardCardMeta {
final IconData icon;
final Color color;
_DashboardCardMeta(this.icon, this.color);
const _DashboardCardMeta(this.icon, this.color);
}

View File

@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart';
import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
// === Your 3 Screens ===
import 'package:on_field_work/view/taskPlanning/daily_progress_report.dart';
import 'package:on_field_work/view/taskPlanning/daily_task_planning.dart';
class InfraProjectsMainScreen extends StatefulWidget {
const InfraProjectsMainScreen({super.key});
@override
State<InfraProjectsMainScreen> createState() =>
_InfraProjectsMainScreenState();
}
class _InfraProjectsMainScreenState extends State<InfraProjectsMainScreen>
with SingleTickerProviderStateMixin, UIMixin {
late TabController _tabController;
final DynamicMenuController menuController = Get.find<DynamicMenuController>();
// Final tab list after filtering
final List<_InfraTab> _tabs = [];
@override
void initState() {
super.initState();
_prepareTabs();
}
void _prepareTabs() {
// Use the same permission logic used in your dashboard_cards
final allowedMenu = menuController.menuItems.where((m) => m.available);
if (allowedMenu.any((m) => m.id == MenuItems.dailyTaskPlanning)) {
_tabs.add(
_InfraTab(
name: "Task Planning",
view: DailyTaskPlanningScreen(),
),
);
}
if (allowedMenu.any((m) => m.id == MenuItems.dailyProgressReport)) {
_tabs.add(
_InfraTab(
name: "Task Progress",
view: DailyProgressReportScreen(),
),
);
}
_tabController = TabController(length: _tabs.length, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF1F1F1),
appBar: CustomAppBar(
title: "Infra Projects",
onBackPressed: () => Get.back(),
backgroundColor: appBarColor,
),
body: Stack(
children: [
// Top faded gradient
Container(
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
SafeArea(
top: false,
bottom: true,
child: Column(
children: [
// PILL TABS
PillTabBar(
controller: _tabController,
tabs: _tabs.map((e) => e.name).toList(),
selectedColor: contentTheme.primary,
unselectedColor: Colors.grey.shade600,
indicatorColor: contentTheme.primary,
),
// TAB CONTENT
Expanded(
child: TabBarView(
controller: _tabController,
children: _tabs.map((e) => e.view).toList(),
),
),
],
),
),
],
),
);
}
}
/// INTERNAL MODEL
class _InfraTab {
final String name;
final Widget view;
_InfraTab({required this.name, required this.view});
}