diff --git a/lib/controller/attendance/attendance_screen_controller.dart b/lib/controller/attendance/attendance_screen_controller.dart index 91b6c8f..f05999b 100644 --- a/lib/controller/attendance/attendance_screen_controller.dart +++ b/lib/controller/attendance/attendance_screen_controller.dart @@ -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 attendances = []; - List projects = []; - List employees = []; - List attendanceLogs = []; - List regularizationLogs = []; - List attendenceLogsView = []; + final List attendances = []; + final List projects = []; + final List employees = []; + final List attendanceLogs = []; + final List regularizationLogs = + []; + final List attendenceLogsView = + []; // ------------------ Organizations ------------------ - List organizations = []; + final List organizations = []; 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 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 = {}.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 uploadingStates = {}.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 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 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 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 refreshDataFromNotification({String? projectId}) async { projectId ??= Get.find().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 fetchTodaysAttendance(String? projectId) async { @@ -114,19 +130,35 @@ class AttendanceController extends GetxController { isLoadingEmployees.value = true; - final response = await ApiService.getTodaysAttendance( + final List? 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( + (dynamic e) => EmployeeModel.fromJson( + e as Map, + ), + ) + .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 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()) { + final DashboardController dashboardController = + Get.find(); + 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 _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? compressedBytes = + await compressImageToUnder100KB(timestampedFile); + if (compressedBytes == null) { + logSafe( + 'Image compression failed.', + level: LogLevel.error, + ); + return null; + } + + // FIX: convert List -> Uint8List + final Uint8List compressedUint8List = Uint8List.fromList(compressedBytes); + + final File compressedFile = + await saveCompressedImageToFile(compressedUint8List); + return XFile(compressedFile.path); + } + + Future _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 fetchAttendanceLogs(String? projectId, - {DateTime? dateFrom, DateTime? dateTo}) async { + Future fetchAttendanceLogs( + String? projectId, { + DateTime? dateFrom, + DateTime? dateTo, + }) async { if (projectId == null) return; isLoadingAttendanceLogs.value = true; - final response = await ApiService.getAttendanceLogs( + final List? 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( + (dynamic e) => AttendanceLogModel.fromJson( + e as Map, + ), + ) + .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> groupLogsByCheckInDate() { - final groupedLogs = >{}; + final Map> groupedLogs = + >{}; - 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, + () => [], + )..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>> sortedEntries = + groupedLogs.entries.toList() + ..sort( + (MapEntry> a, + MapEntry> b) { + if (a.key == 'Unknown') return 1; + if (b.key == 'Unknown') return -1; - return Map>.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>.fromEntries( + sortedEntries, + ); } // ------------------ Regularization Logs ------------------ @@ -307,17 +445,31 @@ class AttendanceController extends GetxController { isLoadingRegularizationLogs.value = true; - final response = await ApiService.getRegularizationLogs( + final List? 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( + (dynamic e) => RegularizationLogModel.fromJson( + e as Map, + ), + ) + .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? 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( + (dynamic e) => AttendanceLogViewModel.fromJson( + e as Map, + ), + ) + .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 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().selectedProject?.id, diff --git a/lib/controller/dashboard/dashboard_controller.dart b/lib/controller/dashboard/dashboard_controller.dart index e6ed81b..9e68ad3 100644 --- a/lib/controller/dashboard/dashboard_controller.dart +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -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 expenseTypes = [].obs; final Rx selectedExpenseType = Rx(null); + final isLoadingEmployees = true.obs; +// DashboardController + final RxList employees = [].obs; + final uploadingStates = {}.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(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 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; diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index fae9abc..328d7fe 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -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"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 37f5b83..6a70f6f 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -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?> 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) { + return [EmployeeModel.fromJson(data)]; + } else if (data is List) { + return data.map((e) => EmployeeModel.fromJson(e)).toList(); + } + + return null; + } + static Future?> getRegularizationLogs( String projectId, { String? organizationId, diff --git a/lib/helpers/utils/permission_constants.dart b/lib/helpers/utils/permission_constants.dart index b5e3b2c..51867b8 100644 --- a/lib/helpers/utils/permission_constants.dart +++ b/lib/helpers/utils/permission_constants.dart @@ -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. diff --git a/lib/model/attendance/attendence_action_button.dart b/lib/model/attendance/attendence_action_button.dart index c65e81b..a2b883a 100644 --- a/lib/model/attendance/attendence_action_button.dart +++ b/lib/model/attendance/attendence_action_button.dart @@ -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, diff --git a/lib/view/Attendence/attendance_screen.dart b/lib/view/Attendence/attendance_screen.dart index 56f2d16..e287267 100644 --- a/lib/view/Attendence/attendance_screen.dart +++ b/lib/view/Attendence/attendance_screen.dart @@ -38,11 +38,12 @@ class _AttendanceScreenState extends State void initState() { super.initState(); - // Watch permissions loaded ever(permissionController.permissionsLoaded, (loaded) { if (loaded == true && !_tabsInitialized) { - _initializeTabs(); - setState(() {}); + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeTabs(); + setState(() {}); + }); } }); diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index f58a6e1..7f3a0db 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -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 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(); bool hasMpin = true; @@ -37,12 +45,14 @@ class _DashboardScreenState extends State with UIMixin { Future _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 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 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 cardOrder = [ MenuItems.attendance, MenuItems.employees, MenuItems.dailyTaskPlanning, @@ -272,9 +239,10 @@ class _DashboardScreenState extends State with UIMixin { MenuItems.finance, MenuItems.documents, MenuItems.serviceProjects, + MenuItems.infraProjects, ]; - final meta = { + final Map meta = { MenuItems.attendance: _DashboardCardMeta(LucideIcons.scan_face, contentTheme.success), MenuItems.employees: @@ -286,97 +254,142 @@ class _DashboardScreenState extends State 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 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 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 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 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 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 with UIMixin { ); } - //--------------------------------------------------------------------------- - // MAIN UI - //--------------------------------------------------------------------------- + // --------------------------------------------------------------------------- + // Build + // --------------------------------------------------------------------------- @override Widget build(BuildContext context) { @@ -460,18 +553,23 @@ class _DashboardScreenState extends State 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 with UIMixin { class _DashboardCardMeta { final IconData icon; final Color color; - _DashboardCardMeta(this.icon, this.color); + + const _DashboardCardMeta(this.icon, this.color); } diff --git a/lib/view/infraProject/infra_project_screen.dart b/lib/view/infraProject/infra_project_screen.dart new file mode 100644 index 0000000..6a5bdc5 --- /dev/null +++ b/lib/view/infraProject/infra_project_screen.dart @@ -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 createState() => + _InfraProjectsMainScreenState(); +} + +class _InfraProjectsMainScreenState extends State + with SingleTickerProviderStateMixin, UIMixin { + late TabController _tabController; + + final DynamicMenuController menuController = Get.find(); + + // 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}); +}