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:
parent
cf85c17d75
commit
03e3e7b5db
@ -1,37 +1,41 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:intl/intl.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/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/my_image_compressor.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.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/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_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/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 {
|
class AttendanceController extends GetxController {
|
||||||
// ------------------ Data Models ------------------
|
// ------------------ Data Models ------------------
|
||||||
List<AttendanceModel> attendances = [];
|
final List<AttendanceModel> attendances = <AttendanceModel>[];
|
||||||
List<ProjectModel> projects = [];
|
final List<ProjectModel> projects = <ProjectModel>[];
|
||||||
List<EmployeeModel> employees = [];
|
final List<EmployeeModel> employees = <EmployeeModel>[];
|
||||||
List<AttendanceLogModel> attendanceLogs = [];
|
final List<AttendanceLogModel> attendanceLogs = <AttendanceLogModel>[];
|
||||||
List<RegularizationLogModel> regularizationLogs = [];
|
final List<RegularizationLogModel> regularizationLogs =
|
||||||
List<AttendanceLogViewModel> attendenceLogsView = [];
|
<RegularizationLogModel>[];
|
||||||
|
final List<AttendanceLogViewModel> attendenceLogsView =
|
||||||
|
<AttendanceLogViewModel>[];
|
||||||
|
|
||||||
// ------------------ Organizations ------------------
|
// ------------------ Organizations ------------------
|
||||||
List<Organization> organizations = [];
|
final List<Organization> organizations = <Organization>[];
|
||||||
Organization? selectedOrganization;
|
Organization? selectedOrganization;
|
||||||
final isLoadingOrganizations = false.obs;
|
final RxBool isLoadingOrganizations = false.obs;
|
||||||
|
|
||||||
// ------------------ States ------------------
|
// ------------------ States ------------------
|
||||||
String selectedTab = 'todaysAttendance';
|
String selectedTab = 'todaysAttendance';
|
||||||
@ -42,16 +46,17 @@ class AttendanceController extends GetxController {
|
|||||||
final Rx<DateTime> endDateAttendance =
|
final Rx<DateTime> endDateAttendance =
|
||||||
DateTime.now().subtract(const Duration(days: 1)).obs;
|
DateTime.now().subtract(const Duration(days: 1)).obs;
|
||||||
|
|
||||||
final isLoading = true.obs;
|
final RxBool isLoading = true.obs;
|
||||||
final isLoadingProjects = true.obs;
|
final RxBool isLoadingProjects = true.obs;
|
||||||
final isLoadingEmployees = true.obs;
|
final RxBool isLoadingEmployees = true.obs;
|
||||||
final isLoadingAttendanceLogs = true.obs;
|
final RxBool isLoadingAttendanceLogs = true.obs;
|
||||||
final isLoadingRegularizationLogs = true.obs;
|
final RxBool isLoadingRegularizationLogs = true.obs;
|
||||||
final isLoadingLogView = true.obs;
|
final RxBool isLoadingLogView = true.obs;
|
||||||
final uploadingStates = <String, RxBool>{}.obs;
|
|
||||||
var showPendingOnly = false.obs;
|
|
||||||
|
|
||||||
final searchQuery = ''.obs;
|
final RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|
||||||
|
final RxBool showPendingOnly = false.obs;
|
||||||
|
final RxString searchQuery = ''.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -64,35 +69,43 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _setDefaultDateRange() {
|
void _setDefaultDateRange() {
|
||||||
final today = DateTime.now();
|
final DateTime today = DateTime.now();
|
||||||
startDateAttendance.value = today.subtract(const Duration(days: 7));
|
startDateAttendance.value = today.subtract(const Duration(days: 7));
|
||||||
endDateAttendance.value = today.subtract(const Duration(days: 1));
|
endDateAttendance.value = today.subtract(const Duration(days: 1));
|
||||||
logSafe(
|
logSafe(
|
||||||
"Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}");
|
'Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Computed Filters ------------------
|
// ------------------ Computed Filters ------------------
|
||||||
List<EmployeeModel> get filteredEmployees {
|
List<EmployeeModel> get filteredEmployees {
|
||||||
if (searchQuery.value.isEmpty) return employees;
|
final String query = searchQuery.value.trim().toLowerCase();
|
||||||
|
if (query.isEmpty) return employees;
|
||||||
return employees
|
return employees
|
||||||
.where((e) =>
|
.where(
|
||||||
e.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
|
(EmployeeModel e) => e.name.toLowerCase().contains(query),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<AttendanceLogModel> get filteredLogs {
|
List<AttendanceLogModel> get filteredLogs {
|
||||||
if (searchQuery.value.isEmpty) return attendanceLogs;
|
final String query = searchQuery.value.trim().toLowerCase();
|
||||||
|
if (query.isEmpty) return attendanceLogs;
|
||||||
return attendanceLogs
|
return attendanceLogs
|
||||||
.where((log) =>
|
.where(
|
||||||
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
|
(AttendanceLogModel log) => log.name.toLowerCase().contains(query),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RegularizationLogModel> get filteredRegularizationLogs {
|
List<RegularizationLogModel> get filteredRegularizationLogs {
|
||||||
if (searchQuery.value.isEmpty) return regularizationLogs;
|
final String query = searchQuery.value.trim().toLowerCase();
|
||||||
|
if (query.isEmpty) return regularizationLogs;
|
||||||
return regularizationLogs
|
return regularizationLogs
|
||||||
.where((log) =>
|
.where(
|
||||||
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
|
(RegularizationLogModel log) =>
|
||||||
|
log.name.toLowerCase().contains(query),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,13 +113,16 @@ class AttendanceController extends GetxController {
|
|||||||
Future<void> refreshDataFromNotification({String? projectId}) async {
|
Future<void> refreshDataFromNotification({String? projectId}) async {
|
||||||
projectId ??= Get.find<ProjectController>().selectedProject?.id;
|
projectId ??= Get.find<ProjectController>().selectedProject?.id;
|
||||||
if (projectId == null) {
|
if (projectId == null) {
|
||||||
logSafe("No project selected for attendance refresh from notification",
|
logSafe(
|
||||||
level: LogLevel.warning);
|
'No project selected for attendance refresh from notification',
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await fetchProjectData(projectId);
|
await fetchProjectData(projectId);
|
||||||
logSafe(
|
logSafe(
|
||||||
"Attendance data refreshed from notification for project $projectId");
|
'Attendance data refreshed from notification for project $projectId',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchTodaysAttendance(String? projectId) async {
|
Future<void> fetchTodaysAttendance(String? projectId) async {
|
||||||
@ -114,19 +130,35 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
isLoadingEmployees.value = true;
|
isLoadingEmployees.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getTodaysAttendance(
|
final List<dynamic>? response = await ApiService.getTodaysAttendance(
|
||||||
projectId,
|
projectId,
|
||||||
organizationId: selectedOrganization?.id,
|
organizationId: selectedOrganization?.id,
|
||||||
);
|
);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
employees = response.map((e) => EmployeeModel.fromJson(e)).toList();
|
employees
|
||||||
for (var emp in 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;
|
uploadingStates[emp.id] = false.obs;
|
||||||
}
|
}
|
||||||
logSafe("Employees fetched: ${employees.length} for project $projectId");
|
|
||||||
|
logSafe(
|
||||||
|
'Employees fetched: ${employees.length} for project $projectId',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to fetch employees for project $projectId",
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Failed to fetch employees for project $projectId',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingEmployees.value = false;
|
isLoadingEmployees.value = false;
|
||||||
@ -135,14 +167,22 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
Future<void> fetchOrganizations(String projectId) async {
|
Future<void> fetchOrganizations(String projectId) async {
|
||||||
isLoadingOrganizations.value = true;
|
isLoadingOrganizations.value = true;
|
||||||
|
|
||||||
|
// Keep original return type inference from your ApiService
|
||||||
final response = await ApiService.getAssignedOrganizations(projectId);
|
final response = await ApiService.getAssignedOrganizations(projectId);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
organizations = response.data;
|
organizations
|
||||||
logSafe("Organizations fetched: ${organizations.length}");
|
..clear()
|
||||||
|
..addAll(response.data);
|
||||||
|
logSafe('Organizations fetched: ${organizations.length}');
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to fetch organizations for project $projectId",
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Failed to fetch organizations for project $projectId',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingOrganizations.value = false;
|
isLoadingOrganizations.value = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
@ -152,61 +192,43 @@ class AttendanceController extends GetxController {
|
|||||||
String id,
|
String id,
|
||||||
String employeeId,
|
String employeeId,
|
||||||
String projectId, {
|
String projectId, {
|
||||||
String comment = "Marked via mobile app",
|
String comment = 'Marked via mobile app',
|
||||||
required int action,
|
required int action,
|
||||||
bool imageCapture = true,
|
bool imageCapture = true,
|
||||||
String? markTime,
|
String? markTime,
|
||||||
String? date,
|
String? date,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
uploadingStates[employeeId]?.value = true;
|
_setUploading(employeeId, true);
|
||||||
|
|
||||||
XFile? image;
|
final XFile? image = await _captureAndPrepareImage(
|
||||||
if (imageCapture) {
|
employeeId: employeeId,
|
||||||
image = await ImagePicker()
|
imageCapture: imageCapture,
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await _handleLocationPermission()) return false;
|
|
||||||
final position = await Geolocator.getCurrentPosition(
|
|
||||||
desiredAccuracy: LocationAccuracy.high);
|
|
||||||
|
|
||||||
final imageName = imageCapture
|
|
||||||
? ApiService.generateImageName(employeeId, employees.length + 1)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
final now = DateTime.now();
|
|
||||||
DateTime effectiveDate = now;
|
|
||||||
|
|
||||||
if (action == 1) {
|
|
||||||
final log = attendanceLogs.firstWhereOrNull(
|
|
||||||
(log) => log.employeeId == employeeId && log.checkOut == null,
|
|
||||||
);
|
);
|
||||||
if (log?.checkIn != null) effectiveDate = log!.checkIn!;
|
if (imageCapture && image == null) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now);
|
final Position? position = await _getCurrentPositionSafely();
|
||||||
final formattedDate =
|
if (position == null) return false;
|
||||||
|
|
||||||
|
final String imageName = imageCapture
|
||||||
|
? ApiService.generateImageName(
|
||||||
|
employeeId,
|
||||||
|
employees.length + 1,
|
||||||
|
)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
final DateTime effectiveDate =
|
||||||
|
_resolveEffectiveDateForAction(action, employeeId);
|
||||||
|
|
||||||
|
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);
|
date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
|
||||||
|
|
||||||
final result = await ApiService.uploadAttendanceImage(
|
final bool result = await ApiService.uploadAttendanceImage(
|
||||||
id,
|
id,
|
||||||
employeeId,
|
employeeId,
|
||||||
image,
|
image,
|
||||||
@ -221,15 +243,99 @@ class AttendanceController extends GetxController {
|
|||||||
date: formattedDate,
|
date: formattedDate,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
logSafe(
|
logSafe(
|
||||||
"Attendance uploaded for $employeeId, action: $action, date: $formattedDate");
|
'Attendance uploaded for $employeeId, action: $action, date: $formattedDate',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Get.isRegistered<DashboardController>()) {
|
||||||
|
final DashboardController dashboardController =
|
||||||
|
Get.find<DashboardController>();
|
||||||
|
await dashboardController.fetchTodaysAttendance(projectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
logSafe("Error uploading attendance",
|
logSafe(
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
'Error uploading attendance',
|
||||||
|
level: LogLevel.error,
|
||||||
|
error: e,
|
||||||
|
stackTrace: stacktrace,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} 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) {
|
if (permission == LocationPermission.denied) {
|
||||||
permission = await Geolocator.requestPermission();
|
permission = await Geolocator.requestPermission();
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
logSafe('Location permissions are denied', level: LogLevel.warning);
|
logSafe(
|
||||||
|
'Location permissions are denied',
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permission == LocationPermission.deniedForever) {
|
if (permission == LocationPermission.deniedForever) {
|
||||||
logSafe('Location permissions are permanently denied',
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Location permissions are permanently denied',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,25 +365,40 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Attendance Logs ------------------
|
// ------------------ Attendance Logs ------------------
|
||||||
Future<void> fetchAttendanceLogs(String? projectId,
|
Future<void> fetchAttendanceLogs(
|
||||||
{DateTime? dateFrom, DateTime? dateTo}) async {
|
String? projectId, {
|
||||||
|
DateTime? dateFrom,
|
||||||
|
DateTime? dateTo,
|
||||||
|
}) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoadingAttendanceLogs.value = true;
|
isLoadingAttendanceLogs.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getAttendanceLogs(
|
final List<dynamic>? response = await ApiService.getAttendanceLogs(
|
||||||
projectId,
|
projectId,
|
||||||
dateFrom: dateFrom,
|
dateFrom: dateFrom,
|
||||||
dateTo: dateTo,
|
dateTo: dateTo,
|
||||||
organizationId: selectedOrganization?.id,
|
organizationId: selectedOrganization?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
attendanceLogs =
|
attendanceLogs
|
||||||
response.map((e) => AttendanceLogModel.fromJson(e)).toList();
|
..clear()
|
||||||
logSafe("Attendance logs fetched: ${attendanceLogs.length}");
|
..addAll(
|
||||||
|
response
|
||||||
|
.map<AttendanceLogModel>(
|
||||||
|
(dynamic e) => AttendanceLogModel.fromJson(
|
||||||
|
e as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
logSafe('Attendance logs fetched: ${attendanceLogs.length}');
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to fetch attendance logs for project $projectId",
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Failed to fetch attendance logs for project $projectId',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingAttendanceLogs.value = false;
|
isLoadingAttendanceLogs.value = false;
|
||||||
@ -280,25 +406,37 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
||||||
final groupedLogs = <String, List<AttendanceLogModel>>{};
|
final Map<String, List<AttendanceLogModel>> groupedLogs =
|
||||||
|
<String, List<AttendanceLogModel>>{};
|
||||||
|
|
||||||
for (var logItem in attendanceLogs) {
|
for (final AttendanceLogModel logItem in attendanceLogs) {
|
||||||
final checkInDate = logItem.checkIn != null
|
final String checkInDate = logItem.checkIn != null
|
||||||
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
|
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
|
||||||
: 'Unknown';
|
: 'Unknown';
|
||||||
groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem);
|
|
||||||
|
groupedLogs.putIfAbsent(
|
||||||
|
checkInDate,
|
||||||
|
() => <AttendanceLogModel>[],
|
||||||
|
)..add(logItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
final sortedEntries = groupedLogs.entries.toList()
|
final List<MapEntry<String, List<AttendanceLogModel>>> sortedEntries =
|
||||||
..sort((a, b) {
|
groupedLogs.entries.toList()
|
||||||
|
..sort(
|
||||||
|
(MapEntry<String, List<AttendanceLogModel>> a,
|
||||||
|
MapEntry<String, List<AttendanceLogModel>> b) {
|
||||||
if (a.key == 'Unknown') return 1;
|
if (a.key == 'Unknown') return 1;
|
||||||
if (b.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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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 ------------------
|
// ------------------ Regularization Logs ------------------
|
||||||
@ -307,17 +445,31 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
isLoadingRegularizationLogs.value = true;
|
isLoadingRegularizationLogs.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getRegularizationLogs(
|
final List<dynamic>? response = await ApiService.getRegularizationLogs(
|
||||||
projectId,
|
projectId,
|
||||||
organizationId: selectedOrganization?.id,
|
organizationId: selectedOrganization?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
regularizationLogs =
|
regularizationLogs
|
||||||
response.map((e) => RegularizationLogModel.fromJson(e)).toList();
|
..clear()
|
||||||
logSafe("Regularization logs fetched: ${regularizationLogs.length}");
|
..addAll(
|
||||||
|
response
|
||||||
|
.map<RegularizationLogModel>(
|
||||||
|
(dynamic e) => RegularizationLogModel.fromJson(
|
||||||
|
e as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
logSafe(
|
||||||
|
'Regularization logs fetched: ${regularizationLogs.length}',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to fetch regularization logs for project $projectId",
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Failed to fetch regularization logs for project $projectId',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingRegularizationLogs.value = false;
|
isLoadingRegularizationLogs.value = false;
|
||||||
@ -330,16 +482,33 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
isLoadingLogView.value = true;
|
isLoadingLogView.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getAttendanceLogView(id);
|
final List<dynamic>? response = await ApiService.getAttendanceLogView(id);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
attendenceLogsView =
|
attendenceLogsView
|
||||||
response.map((e) => AttendanceLogViewModel.fromJson(e)).toList();
|
..clear()
|
||||||
attendenceLogsView.sort((a, b) => (b.activityTime ?? DateTime(2000))
|
..addAll(
|
||||||
.compareTo(a.activityTime ?? DateTime(2000)));
|
response
|
||||||
logSafe("Attendance log view fetched for ID: $id");
|
.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 {
|
} else {
|
||||||
logSafe("Failed to fetch attendance log view for ID $id",
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Failed to fetch attendance log view for ID $id',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingLogView.value = false;
|
isLoadingLogView.value = false;
|
||||||
@ -375,16 +544,19 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logSafe(
|
logSafe(
|
||||||
"Project data fetched for project ID: $projectId, tab: $selectedTab");
|
'Project data fetched for project ID: $projectId, tab: $selectedTab',
|
||||||
|
);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ UI Interaction ------------------
|
// ------------------ UI Interaction ------------------
|
||||||
Future<void> selectDateRangeForAttendance(
|
Future<void> selectDateRangeForAttendance(
|
||||||
BuildContext context, AttendanceController controller) async {
|
BuildContext context,
|
||||||
final today = DateTime.now();
|
AttendanceController controller,
|
||||||
|
) async {
|
||||||
|
final DateTime today = DateTime.now();
|
||||||
|
|
||||||
final picked = await showDateRangePicker(
|
final DateTimeRange? picked = await showDateRangePicker(
|
||||||
context: context,
|
context: context,
|
||||||
firstDate: DateTime(2022),
|
firstDate: DateTime(2022),
|
||||||
lastDate: today.subtract(const Duration(days: 1)),
|
lastDate: today.subtract(const Duration(days: 1)),
|
||||||
@ -399,7 +571,8 @@ class AttendanceController extends GetxController {
|
|||||||
endDateAttendance.value = picked.end;
|
endDateAttendance.value = picked.end;
|
||||||
|
|
||||||
logSafe(
|
logSafe(
|
||||||
"Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}");
|
'Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}',
|
||||||
|
);
|
||||||
|
|
||||||
await controller.fetchAttendanceLogs(
|
await controller.fetchAttendanceLogs(
|
||||||
Get.find<ProjectController>().selectedProject?.id,
|
Get.find<ProjectController>().selectedProject?.id,
|
||||||
|
|||||||
@ -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/expense_type_report_model.dart';
|
||||||
import 'package:on_field_work/model/dashboard/monthly_expence_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/expense/expense_type_model.dart';
|
||||||
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
|
|
||||||
class DashboardController extends GetxController {
|
class DashboardController extends GetxController {
|
||||||
// =========================
|
// =========================
|
||||||
@ -81,6 +82,10 @@ class DashboardController extends GetxController {
|
|||||||
final RxInt selectedMonthsCount = 12.obs;
|
final RxInt selectedMonthsCount = 12.obs;
|
||||||
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
|
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
|
||||||
final Rx<ExpenseTypeModel?> selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
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) {
|
void updateSelectedExpenseType(ExpenseTypeModel? type) {
|
||||||
selectedExpenseType.value = type;
|
selectedExpenseType.value = type;
|
||||||
@ -100,24 +105,35 @@ class DashboardController extends GetxController {
|
|||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
||||||
logSafe(
|
logSafe(
|
||||||
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
|
'DashboardController initialized',
|
||||||
level: LogLevel.info,
|
level: LogLevel.info,
|
||||||
);
|
);
|
||||||
|
|
||||||
fetchAllDashboardData();
|
// React to project selection
|
||||||
|
|
||||||
// React to project change
|
|
||||||
ever<String>(projectController.selectedProjectId, (id) {
|
ever<String>(projectController.selectedProjectId, (id) {
|
||||||
|
if (id.isNotEmpty) {
|
||||||
|
logSafe('Project selected: $id', level: LogLevel.info);
|
||||||
fetchAllDashboardData();
|
fetchAllDashboardData();
|
||||||
|
fetchTodaysAttendance(id);
|
||||||
|
} else {
|
||||||
|
logSafe('No project selected yet.', level: LogLevel.warning);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// React to expense report date changes
|
||||||
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
|
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
|
||||||
|
if (projectController.selectedProjectId.value.isNotEmpty) {
|
||||||
fetchExpenseTypeReport(
|
fetchExpenseTypeReport(
|
||||||
startDate: expenseReportStartDate.value,
|
startDate: expenseReportStartDate.value,
|
||||||
endDate: expenseReportEndDate.value,
|
endDate: expenseReportEndDate.value,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// React to range changes
|
|
||||||
|
// React to attendance range changes
|
||||||
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
|
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
|
||||||
|
|
||||||
|
// React to project range changes
|
||||||
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
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) {
|
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
|
||||||
selectedMonthlyExpenseDuration.value = duration;
|
selectedMonthlyExpenseDuration.value = duration;
|
||||||
|
|
||||||
@ -359,7 +395,8 @@ class DashboardController extends GetxController {
|
|||||||
level: LogLevel.info);
|
level: LogLevel.info);
|
||||||
} else {
|
} else {
|
||||||
expenseTypeReportData.value = null;
|
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) {
|
} catch (e, st) {
|
||||||
expenseTypeReportData.value = null;
|
expenseTypeReportData.value = null;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
class ApiEndpoints {
|
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://api.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://devapi.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://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";
|
static const String getMasterCurrencies = "/Master/currencies/list";
|
||||||
@ -44,6 +44,7 @@ class ApiEndpoints {
|
|||||||
static const String getProjects = "/project/list";
|
static const String getProjects = "/project/list";
|
||||||
static const String getGlobalProjects = "/project/list/basic";
|
static const String getGlobalProjects = "/project/list/basic";
|
||||||
static const String getTodaysAttendance = "/attendance/project/team";
|
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 getAttendanceLogs = "/attendance/project/log";
|
||||||
static const String getAttendanceLogView = "/attendance/log/attendance";
|
static const String getAttendanceLogView = "/attendance/log/attendance";
|
||||||
static const String getRegularizationLogs = "/attendance/regularize";
|
static const String getRegularizationLogs = "/attendance/regularize";
|
||||||
|
|||||||
@ -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/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_status_response.dart';
|
||||||
import 'package:on_field_work/model/service_project/job_comments.dart';
|
import 'package:on_field_work/model/service_project/job_comments.dart';
|
||||||
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
static const bool enableLogs = true;
|
static const bool enableLogs = true;
|
||||||
@ -3320,6 +3321,30 @@ class ApiService {
|
|||||||
res != null ? _parseResponse(res, label: 'Employees') : null);
|
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(
|
static Future<List<dynamic>?> getRegularizationLogs(
|
||||||
String projectId, {
|
String projectId, {
|
||||||
String? organizationId,
|
String? organizationId,
|
||||||
|
|||||||
@ -163,6 +163,9 @@ class MenuItems {
|
|||||||
|
|
||||||
/// Service Projects
|
/// Service Projects
|
||||||
static const String serviceProjects = "7faddfe7-994b-4712-91c2-32ba44129d9b";
|
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.
|
/// Contains all job status IDs used across the application.
|
||||||
|
|||||||
@ -271,13 +271,9 @@ class AttendanceActionButtonUI extends StatelessWidget {
|
|||||||
textStyle: const TextStyle(fontSize: 12),
|
textStyle: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
child: isUploading
|
child: isUploading
|
||||||
? Container(
|
? const Text(
|
||||||
width: 60,
|
'Loading...',
|
||||||
height: 14,
|
style: TextStyle(fontSize: 12, color: Colors.white),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.5),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: Row(
|
: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|||||||
@ -38,11 +38,12 @@ class _AttendanceScreenState extends State<AttendanceScreen>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
// Watch permissions loaded
|
|
||||||
ever(permissionController.permissionsLoaded, (loaded) {
|
ever(permissionController.permissionsLoaded, (loaded) {
|
||||||
if (loaded == true && !_tabsInitialized) {
|
if (loaded == true && !_tabsInitialized) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_initializeTabs();
|
_initializeTabs();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,23 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
import 'package:get/get.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/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/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/services/storage/local_storage.dart';
|
||||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/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_breakdown_chart.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.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/dashbaord/monthly_expense_dashboard_chart.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';
|
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';
|
||||||
|
|
||||||
class DashboardScreen extends StatefulWidget {
|
class DashboardScreen extends StatefulWidget {
|
||||||
const DashboardScreen({super.key});
|
const DashboardScreen({super.key});
|
||||||
@ -24,7 +29,10 @@ class DashboardScreen extends StatefulWidget {
|
|||||||
class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||||
final DashboardController dashboardController =
|
final DashboardController dashboardController =
|
||||||
Get.put(DashboardController(), permanent: true);
|
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>();
|
final ProjectController projectController = Get.find<ProjectController>();
|
||||||
|
|
||||||
bool hasMpin = true;
|
bool hasMpin = true;
|
||||||
@ -37,11 +45,13 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
|
|
||||||
Future<void> _checkMpinStatus() async {
|
Future<void> _checkMpinStatus() async {
|
||||||
hasMpin = await LocalStorage.getIsMpin();
|
hasMpin = await LocalStorage.getIsMpin();
|
||||||
if (mounted) setState(() {});
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// REUSABLE CARD (smaller, minimal)
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Widget _cardWrapper({required Widget child}) {
|
Widget _cardWrapper({required Widget child}) {
|
||||||
@ -56,17 +66,13 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
color: Colors.black12.withOpacity(.05),
|
color: Colors.black12.withOpacity(.05),
|
||||||
blurRadius: 12,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// SECTION TITLE
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Widget _sectionTitle(String title) {
|
Widget _sectionTitle(String title) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 4, bottom: 8),
|
padding: const EdgeInsets.only(left: 4, bottom: 8),
|
||||||
@ -81,13 +87,48 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _conditionalQuickActionCard() {
|
// ---------------------------------------------------------------------------
|
||||||
String status = "1"; // <-- change as needed
|
// Quick Actions
|
||||||
bool isCheckedIn = status == "O";
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Button color remains the same
|
Widget _quickActions() {
|
||||||
Color buttonColor =
|
return Column(
|
||||||
isCheckedIn ? Colors.red.shade700 : Colors.green.shade700;
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_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(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@ -95,175 +136,101 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
contentTheme.primary.withOpacity(0.3), // lighter/faded
|
contentTheme.primary.withOpacity(0.3),
|
||||||
contentTheme.primary.withOpacity(0.6), // slightly stronger
|
contentTheme.primary.withOpacity(0.6),
|
||||||
],
|
],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black12.withOpacity(0.05),
|
|
||||||
blurRadius: 6,
|
|
||||||
offset: const Offset(0, 3),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Title & Status
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Avatar(
|
||||||
isCheckedIn ? "Checked-In" : "Not Checked-In",
|
firstName: employee.firstName,
|
||||||
style: const TextStyle(
|
lastName: employee.lastName,
|
||||||
fontSize: 16,
|
size: 30,
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
|
MySpacing.width(10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MyText.titleSmall(
|
||||||
|
employee.name,
|
||||||
|
fontWeight: 600,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
MyText.labelSmall(
|
||||||
Icon(
|
employee.designation,
|
||||||
isCheckedIn ? LucideIcons.log_out : LucideIcons.log_in,
|
fontWeight: 500,
|
||||||
color: Colors.white,
|
color: Colors.white70,
|
||||||
size: 24,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
// Description
|
MyText.bodySmall(
|
||||||
Text(
|
statusText,
|
||||||
isCheckedIn
|
fontWeight: 600,
|
||||||
? "You are currently checked-in. Don't forget to check-out after your work."
|
color: Colors.white,
|
||||||
: "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),
|
||||||
|
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),
|
const SizedBox(height: 12),
|
||||||
// Action Button (solid color)
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
AttendanceActionButton(
|
||||||
onPressed: () {
|
employee: employee,
|
||||||
// Check-In / Check-Out action
|
attendanceController: attendanceController,
|
||||||
},
|
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
|
if (isCheckedIn) ...[
|
||||||
|
MySpacing.width(8),
|
||||||
|
AttendanceLogViewButton(
|
||||||
|
employee: employee,
|
||||||
|
attendanceController: attendanceController,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}),
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// QUICK ACTIONS (updated to use the single card)
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Widget _quickActions() {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_sectionTitle("Quick Action"),
|
|
||||||
_conditionalQuickActionCard(),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
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)
|
// Dashboard Modules
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Widget _dashboardCards() {
|
Widget _dashboardModules() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (menuController.isLoading.value) {
|
if (menuController.isLoading.value) {
|
||||||
// Show skeleton instead of CircularProgressIndicator
|
|
||||||
return SkeletonLoaders.dashboardCardsSkeleton(
|
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.attendance,
|
||||||
MenuItems.employees,
|
MenuItems.employees,
|
||||||
MenuItems.dailyTaskPlanning,
|
MenuItems.dailyTaskPlanning,
|
||||||
@ -272,9 +239,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
MenuItems.finance,
|
MenuItems.finance,
|
||||||
MenuItems.documents,
|
MenuItems.documents,
|
||||||
MenuItems.serviceProjects,
|
MenuItems.serviceProjects,
|
||||||
|
MenuItems.infraProjects,
|
||||||
];
|
];
|
||||||
|
|
||||||
final meta = {
|
final Map<String, _DashboardCardMeta> meta = {
|
||||||
MenuItems.attendance:
|
MenuItems.attendance:
|
||||||
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
|
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
|
||||||
MenuItems.employees:
|
MenuItems.employees:
|
||||||
@ -291,93 +259,138 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
_DashboardCardMeta(LucideIcons.file_text, contentTheme.info),
|
_DashboardCardMeta(LucideIcons.file_text, contentTheme.info),
|
||||||
MenuItems.serviceProjects:
|
MenuItems.serviceProjects:
|
||||||
_DashboardCardMeta(LucideIcons.package, contentTheme.info),
|
_DashboardCardMeta(LucideIcons.package, contentTheme.info),
|
||||||
|
MenuItems.infraProjects:
|
||||||
|
_DashboardCardMeta(LucideIcons.building_2, contentTheme.primary),
|
||||||
};
|
};
|
||||||
|
|
||||||
final allowed = {
|
final Map<String, dynamic> allowed = {
|
||||||
for (var m in menuController.menuItems)
|
for (final m in menuController.menuItems)
|
||||||
if (m.available && meta.containsKey(m.id)) m.id: m
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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(
|
GridView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
|
||||||
// **More compact grid**
|
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 4,
|
crossAxisCount: 4,
|
||||||
crossAxisSpacing: 6,
|
crossAxisSpacing: 8,
|
||||||
mainAxisSpacing: 6,
|
mainAxisSpacing: 8,
|
||||||
childAspectRatio: 1.2, // smaller & tighter
|
childAspectRatio: 1.15,
|
||||||
),
|
),
|
||||||
|
|
||||||
itemCount: filtered.length,
|
itemCount: filtered.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final id = filtered[index];
|
final String id = filtered[index];
|
||||||
final item = allowed[id]!;
|
final item = allowed[id]!;
|
||||||
final cardMeta = meta[id]!;
|
final _DashboardCardMeta cardMeta = meta[id]!;
|
||||||
|
|
||||||
final isEnabled =
|
final bool isEnabled =
|
||||||
item.name == "Attendance" ? true : projectSelected;
|
item.name == 'Attendance' ? true : projectSelected;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
Get.defaultDialog(
|
Get.snackbar(
|
||||||
title: "No Project Selected",
|
'Required',
|
||||||
middleText: "Please select a project first.",
|
'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 {
|
} else {
|
||||||
Get.toNamed(item.mobileLink);
|
Get.toNamed(item.mobileLink);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
// **Reduced padding**
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isEnabled ? Colors.white : Colors.grey.shade100,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Colors.black12.withOpacity(.1),
|
color: isEnabled
|
||||||
width: 0.7,
|
? Colors.black12.withOpacity(0.06)
|
||||||
|
: Colors.transparent,
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black12.withOpacity(.05),
|
color: Colors.black.withOpacity(0.03),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
cardMeta.icon,
|
cardMeta.icon,
|
||||||
size: 20,
|
size: 20,
|
||||||
color:
|
color: isEnabled
|
||||||
isEnabled ? cardMeta.color : Colors.grey.shade400,
|
? cardMeta.color
|
||||||
|
: Colors.grey.shade300,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
child: Text(
|
||||||
item.name,
|
item.name,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 9.5,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: isEnabled
|
||||||
color:
|
? FontWeight.w600
|
||||||
isEnabled ? Colors.black87 : Colors.grey.shade600,
|
: FontWeight.w400,
|
||||||
|
color: isEnabled
|
||||||
|
? Colors.black87
|
||||||
|
: Colors.grey.shade400,
|
||||||
|
height: 1.2,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
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(
|
return Container(
|
||||||
margin: const EdgeInsets.only(top: 10),
|
margin: const EdgeInsets.only(top: 10),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
@ -405,17 +496,19 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
constraints:
|
constraints: BoxConstraints(
|
||||||
BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.33),
|
maxHeight: MediaQuery.of(context).size.height * 0.33,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Search project...",
|
hintText: 'Search project...',
|
||||||
isDense: true,
|
isDense: true,
|
||||||
prefixIcon: const Icon(Icons.search),
|
prefixIcon: const Icon(Icons.search),
|
||||||
border:
|
border: OutlineInputBorder(
|
||||||
OutlineInputBorder(borderRadius: BorderRadius.circular(5)),
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@ -428,9 +521,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
dense: true,
|
dense: true,
|
||||||
value: project.id,
|
value: project.id,
|
||||||
groupValue: selectedId,
|
groupValue: selectedId,
|
||||||
onChanged: (v) {
|
onChanged: (value) {
|
||||||
if (v != null) {
|
if (value != null) {
|
||||||
projectController.updateSelectedProject(v);
|
projectController.updateSelectedProject(value);
|
||||||
projectController.isProjectSelectionExpanded.value =
|
projectController.isProjectSelectionExpanded.value =
|
||||||
false;
|
false;
|
||||||
}
|
}
|
||||||
@ -446,7 +539,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// MAIN UI
|
// Build
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -460,18 +553,23 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_projectSelector(),
|
_projectSelector(),
|
||||||
// MySpacing.height(20),
|
|
||||||
// _quickActions(),
|
|
||||||
MySpacing.height(20),
|
MySpacing.height(20),
|
||||||
// The updated module cards
|
_quickActions(),
|
||||||
_dashboardCards(),
|
|
||||||
MySpacing.height(20),
|
MySpacing.height(20),
|
||||||
_sectionTitle("Reports & Analytics"),
|
_dashboardModules(),
|
||||||
_cardWrapper(child: ExpenseTypeReportChart()),
|
MySpacing.height(20),
|
||||||
|
_sectionTitle('Reports & Analytics'),
|
||||||
_cardWrapper(
|
_cardWrapper(
|
||||||
child:
|
child: ExpenseTypeReportChart(),
|
||||||
ExpenseByStatusWidget(controller: dashboardController)),
|
),
|
||||||
_cardWrapper(child: MonthlyExpenseDashboardChart()),
|
_cardWrapper(
|
||||||
|
child: ExpenseByStatusWidget(
|
||||||
|
controller: dashboardController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_cardWrapper(
|
||||||
|
child: MonthlyExpenseDashboardChart(),
|
||||||
|
),
|
||||||
MySpacing.height(20),
|
MySpacing.height(20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -484,5 +582,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
class _DashboardCardMeta {
|
class _DashboardCardMeta {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final Color color;
|
final Color color;
|
||||||
_DashboardCardMeta(this.icon, this.color);
|
|
||||||
|
const _DashboardCardMeta(this.icon, this.color);
|
||||||
}
|
}
|
||||||
|
|||||||
134
lib/view/infraProject/infra_project_screen.dart
Normal file
134
lib/view/infraProject/infra_project_screen.dart
Normal 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});
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user