diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 4049058..ca14f30 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 4049058..ca14f30 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 4049058..ca14f30 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 4049058..ca14f30 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4049058..ca14f30 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/logo/loading_logo.png b/assets/logo/loading_logo.png index 81871d3..eaece50 100644 Binary files a/assets/logo/loading_logo.png and b/assets/logo/loading_logo.png differ diff --git a/assets/logo/logo_dark.png b/assets/logo/logo_dark.png index ba9c246..ca14f30 100644 Binary files a/assets/logo/logo_dark.png and b/assets/logo/logo_dark.png differ diff --git a/assets/logo/logo_dark_small.png b/assets/logo/logo_dark_small.png index 4049058..ca14f30 100644 Binary files a/assets/logo/logo_dark_small.png and b/assets/logo/logo_dark_small.png differ diff --git a/assets/logo/logo_light.png b/assets/logo/logo_light.png index 4049058..ca14f30 100644 Binary files a/assets/logo/logo_light.png and b/assets/logo/logo_light.png differ diff --git a/assets/logo/logo_light_small.png b/assets/logo/logo_light_small.png index 4049058..ca14f30 100644 Binary files a/assets/logo/logo_light_small.png and b/assets/logo/logo_light_small.png differ diff --git a/lib/controller/dashboard/add_employee_controller.dart b/lib/controller/dashboard/add_employee_controller.dart index 56d0d67..defdb20 100644 --- a/lib/controller/dashboard/add_employee_controller.dart +++ b/lib/controller/dashboard/add_employee_controller.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:marco/controller/my_controller.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/services/api_service.dart'; -import 'package:get/get.dart'; import 'package:logger/logger.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; enum Gender { male, @@ -77,16 +77,16 @@ class AddEmployeeController extends MyController { update(); } - Future createEmployees() async { + Future createEmployees() async { logger.i("Starting employee creation..."); if (selectedGender == null || selectedRoleId == null) { logger.w("Missing gender or role."); - Get.snackbar( - "Missing Fields", - "Please select both Gender and Role.", - snackPosition: SnackPosition.BOTTOM, + showAppSnackbar( + title: "Missing Fields", + message: "Please select both Gender and Role.", + type: SnackbarType.warning, ); - return; + return false; } final firstName = basicValidator.getController("first_name")?.text.trim(); @@ -107,13 +107,20 @@ class AddEmployeeController extends MyController { if (response == true) { logger.i("Employee created successfully."); - Get.back(); // Or navigate as needed - Get.snackbar("Success", "Employee created successfully!", - snackPosition: SnackPosition.BOTTOM); + showAppSnackbar( + title: "Success", + message: "Employee created successfully!", + type: SnackbarType.success, + ); + return true; } else { logger.e("Failed to create employee."); - Get.snackbar("Error", "Failed to create employee.", - snackPosition: SnackPosition.BOTTOM); + showAppSnackbar( + title: "Error", + message: "Failed to create employee.", + type: SnackbarType.error, + ); + return false; } } } diff --git a/lib/controller/dashboard/attendance_screen_controller.dart b/lib/controller/dashboard/attendance_screen_controller.dart index 000514e..4a950f2 100644 --- a/lib/controller/dashboard/attendance_screen_controller.dart +++ b/lib/controller/dashboard/attendance_screen_controller.dart @@ -4,7 +4,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:geolocator/geolocator.dart'; import 'package:intl/intl.dart'; import 'package:logger/logger.dart'; - +import 'dart:io'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/model/attendance_model.dart'; import 'package:marco/model/project_model.dart'; @@ -12,6 +12,7 @@ import 'package:marco/model/employee_model.dart'; import 'package:marco/model/attendance_log_model.dart'; import 'package:marco/model/regularization_log_model.dart'; import 'package:marco/model/attendance_log_view_model.dart'; +import 'package:marco/helpers/widgets/my_image_compressor.dart'; final Logger log = Logger(); @@ -28,9 +29,7 @@ class AttendanceController extends GetxController { List regularizationLogs = []; List attendenceLogsView = []; - RxBool isLoading = true.obs; - - // New separate loading states per feature + RxBool isLoading = true.obs; // initially true RxBool isLoadingProjects = true.obs; RxBool isLoadingEmployees = true.obs; RxBool isLoadingAttendanceLogs = true.obs; @@ -47,7 +46,7 @@ class AttendanceController extends GetxController { void _initializeDefaults() { _setDefaultDateRange(); - fetchProjects(); + fetchProjects(); // fetchProjects will set isLoading to false after loading } void _setDefaultDateRange() { @@ -58,9 +57,7 @@ class AttendanceController extends GetxController { } Future _handleLocationPermission() async { - LocationPermission permission; - - permission = await Geolocator.checkPermission(); + LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { @@ -68,34 +65,33 @@ class AttendanceController extends GetxController { return false; } } - if (permission == LocationPermission.deniedForever) { log.e('Location permissions are permanently denied'); return false; } - return true; } Future fetchProjects() async { - // Both old and new loading state set for safety - isLoading.value = true; isLoadingProjects.value = true; + isLoading.value = true; final response = await ApiService.getProjects(); - isLoadingProjects.value = false; - isLoading.value = false; - if (response != null && response.isNotEmpty) { projects = response.map((json) => ProjectModel.fromJson(json)).toList(); selectedProjectId = projects.first.id.toString(); log.i("Projects fetched: ${projects.length} projects loaded."); + await fetchProjectData(selectedProjectId); - update(['attendance_dashboard_controller']); } else { log.w("No project data found or API call failed."); } + + isLoadingProjects.value = false; + isLoading.value = false; + + update(['attendance_dashboard_controller']); } Future fetchProjectData(String? projectId) async { @@ -117,28 +113,23 @@ class AttendanceController extends GetxController { Future fetchEmployeesByProject(String? projectId) async { if (projectId == null) return; - isLoading.value = true; isLoadingEmployees.value = true; final response = await ApiService.getEmployeesByProject(projectId); - isLoadingEmployees.value = false; - isLoading.value = false; - if (response != null) { employees = response.map((json) => EmployeeModel.fromJson(json)).toList(); - // Initialize per-employee uploading state for (var emp in employees) { uploadingStates[emp.id] = false.obs; } - log.i( "Employees fetched: ${employees.length} employees for project $projectId"); update(); } else { log.e("Failed to fetch employees for project $projectId"); } + isLoadingEmployees.value = false; } Future captureAndUploadAttendance( @@ -164,6 +155,16 @@ class AttendanceController extends GetxController { uploadingStates[employeeId]?.value = false; return false; } + final compressedBytes = + await compressImageToUnder100KB(File(image.path)); + if (compressedBytes == null) { + log.e("Image compression failed."); + uploadingStates[employeeId]?.value = false; + return false; + } + + final compressedFile = await saveCompressedImageToFile(compressedBytes); + image = XFile(compressedFile.path); } final hasLocationPermission = await _handleLocationPermission(); @@ -224,12 +225,37 @@ class AttendanceController extends GetxController { selectableDayPredicate: (DateTime day, DateTime? start, DateTime? end) { final dayDateOnly = DateTime(day.year, day.month, day.day); if (dayDateOnly == todayDateOnly) { - return false; + return false; } - return true; + return true; + }, + builder: (BuildContext context, Widget? child) { + return Center( + child: SizedBox( + width: 400, + height: 500, + child: Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: const Color.fromARGB(255, 95, 132, 255), + onPrimary: Colors.white, + onSurface: Colors.teal.shade800, + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: Colors.teal, + ), + ), + dialogTheme: DialogTheme( + backgroundColor: Colors.white, + ), + ), + child: child!, + ), + ), + ); }, ); - if (picked != null) { startDateAttendance = picked.start; endDateAttendance = picked.end; @@ -251,8 +277,8 @@ class AttendanceController extends GetxController { }) async { if (projectId == null) return; - isLoading.value = true; isLoadingAttendanceLogs.value = true; + isLoading.value = true; final response = await ApiService.getAttendanceLogs( projectId, @@ -260,9 +286,6 @@ class AttendanceController extends GetxController { dateTo: dateTo, ); - isLoadingAttendanceLogs.value = false; - isLoading.value = false; - if (response != null) { attendanceLogs = response.map((json) => AttendanceLogModel.fromJson(json)).toList(); @@ -271,6 +294,8 @@ class AttendanceController extends GetxController { } else { log.e("Failed to fetch attendance logs for project $projectId"); } + isLoadingAttendanceLogs.value = false; + isLoading.value = false; } Map> groupLogsByCheckInDate() { @@ -284,8 +309,6 @@ class AttendanceController extends GetxController { groupedLogs.putIfAbsent(checkInDate, () => []); groupedLogs[checkInDate]!.add(logItem); } - - // Sort by date descending final sortedEntries = groupedLogs.entries.toList() ..sort((a, b) { if (a.key == 'Unknown') return 1; @@ -309,14 +332,11 @@ class AttendanceController extends GetxController { }) async { if (projectId == null) return; - isLoading.value = true; isLoadingRegularizationLogs.value = true; + isLoading.value = true; final response = await ApiService.getRegularizationLogs(projectId); - isLoadingRegularizationLogs.value = false; - isLoading.value = false; - if (response != null) { regularizationLogs = response .map((json) => RegularizationLogModel.fromJson(json)) @@ -326,25 +346,23 @@ class AttendanceController extends GetxController { } else { log.e("Failed to fetch regularization logs for project $projectId"); } + isLoadingRegularizationLogs.value = false; + isLoading.value = false; } Future fetchLogsView(String? id) async { if (id == null) return; - isLoading.value = true; isLoadingLogView.value = true; + isLoading.value = true; final response = await ApiService.getAttendanceLogView(id); - isLoadingLogView.value = false; - isLoading.value = false; - if (response != null) { attendenceLogsView = response .map((json) => AttendanceLogViewModel.fromJson(json)) .toList(); - // Sort by activityTime field (latest first) attendenceLogsView.sort((a, b) { if (a.activityTime == null || b.activityTime == null) return 0; return b.activityTime!.compareTo(a.activityTime!); @@ -355,5 +373,8 @@ class AttendanceController extends GetxController { } else { log.e("Failed to fetch attendance log view for ID $id"); } + + isLoadingLogView.value = false; + isLoading.value = false; } } diff --git a/lib/controller/dashboard/daily_task_controller.dart b/lib/controller/dashboard/daily_task_controller.dart index 03ae337..8849c27 100644 --- a/lib/controller/dashboard/daily_task_controller.dart +++ b/lib/controller/dashboard/daily_task_controller.dart @@ -15,9 +15,18 @@ class DailyTaskController extends GetxController { DateTime? endDateTask; List dailyTasks = []; + final RxSet expandedDates = {}.obs; + + void toggleDate(String dateKey) { + if (expandedDates.contains(dateKey)) { + expandedDates.remove(dateKey); + } else { + expandedDates.add(dateKey); + } + } RxBool isLoading = false.obs; - + Map> groupedDailyTasks = {}; @override void onInit() { super.onInit(); @@ -50,47 +59,46 @@ class DailyTaskController extends GetxController { projects = response!.map((json) => ProjectModel.fromJson(json)).toList(); selectedProjectId = projects.first.id.toString(); log.i("Projects fetched: ${projects.length} projects loaded."); - update(); + update(); await fetchTaskData(selectedProjectId); } -Future fetchTaskData(String? projectId) async { - if (projectId == null) return; + Future fetchTaskData(String? projectId) async { + if (projectId == null) return; - isLoading.value = true; - final response = await ApiService.getDailyTasks( - projectId, - dateFrom: startDateTask, - dateTo: endDateTask, - ); - isLoading.value = false; + isLoading.value = true; + final response = await ApiService.getDailyTasks( + projectId, + dateFrom: startDateTask, + dateTo: endDateTask, + ); + isLoading.value = false; - if (response != null) { - Map> groupedTasks = {}; + if (response != null) { + groupedDailyTasks.clear(); - for (var taskJson in response) { - TaskModel task = TaskModel.fromJson(taskJson); - String assignmentDateKey = task.assignmentDate; + for (var taskJson in response) { + TaskModel task = TaskModel.fromJson(taskJson); + String assignmentDateKey = + task.assignmentDate.toIso8601String().split('T')[0]; - if (groupedTasks.containsKey(assignmentDateKey)) { - groupedTasks[assignmentDateKey]?.add(task); - } else { - groupedTasks[assignmentDateKey] = [task]; + if (groupedDailyTasks.containsKey(assignmentDateKey)) { + groupedDailyTasks[assignmentDateKey]?.add(task); + } else { + groupedDailyTasks[assignmentDateKey] = [task]; + } } + + // Flatten the grouped tasks into the existing dailyTasks list + dailyTasks = groupedDailyTasks.values.expand((list) => list).toList(); + + log.i("Daily tasks fetched and grouped: ${dailyTasks.length}"); + + update(); + } else { + log.e("Failed to fetch daily tasks for project $projectId"); } - dailyTasks = groupedTasks.entries - .map((entry) => entry.value) - .expand((taskList) => taskList) - .toList(); - - log.i("Daily tasks fetched and grouped: ${dailyTasks.length}"); - - update(); - } else { - log.e("Failed to fetch daily tasks for project $projectId"); } -} - Future selectDateRangeForTaskData( BuildContext context, @@ -101,7 +109,8 @@ Future fetchTaskData(String? projectId) async { firstDate: DateTime(2022), lastDate: DateTime.now(), initialDateRange: DateTimeRange( - start: startDateTask ?? DateTime.now().subtract(const Duration(days: 7)), + start: + startDateTask ?? DateTime.now().subtract(const Duration(days: 7)), end: endDateTask ?? DateTime.now(), ), ); diff --git a/lib/controller/dashboard/employees_screen_controller.dart b/lib/controller/dashboard/employees_screen_controller.dart index 023e908..bdefcef 100644 --- a/lib/controller/dashboard/employees_screen_controller.dart +++ b/lib/controller/dashboard/employees_screen_controller.dart @@ -4,6 +4,7 @@ import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/model/attendance_model.dart'; import 'package:marco/model/project_model.dart'; import 'package:marco/model/employee_model.dart'; +import 'package:marco/model/employees/employee_details_model.dart'; final Logger log = Logger(); @@ -12,14 +13,18 @@ class EmployeesScreenController extends GetxController { List projects = []; String? selectedProjectId; List employees = []; + List employeeDetails = []; RxBool isLoading = false.obs; RxMap uploadingStates = {}.obs; - + Rxn selectedEmployeeDetails = + Rxn(); + RxBool isLoadingEmployeeDetails = false.obs; @override void onInit() { super.onInit(); fetchAllProjects(); + fetchAllEmployees(); } Future fetchAllProjects() async { @@ -69,8 +74,8 @@ class EmployeesScreenController extends GetxController { }, onEmpty: () { log.w("No employees found for project $projectId."); - employees = []; - update(); + employees = []; + update(); }, onError: (e) => log.e("Error fetching employees for project $projectId: $e"), @@ -99,4 +104,47 @@ class EmployeesScreenController extends GetxController { } } } + + Future fetchEmployeeDetails(String? employeeId) async { + if (employeeId == null || employeeId.isEmpty) return; + + isLoadingEmployeeDetails.value = true; + + await _handleSingleApiCall( + () => ApiService.getEmployeeDetails(employeeId), + onSuccess: (data) { + selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data); + }, + onEmpty: () { + selectedEmployeeDetails.value = null; + }, + onError: (e) { + selectedEmployeeDetails.value = null; + }, + ); + + isLoadingEmployeeDetails.value = false; + } + + Future _handleSingleApiCall( + Future?> Function() apiCall, { + required Function(Map) onSuccess, + required Function() onEmpty, + Function(dynamic error)? onError, + }) async { + try { + final response = await apiCall(); + if (response != null && response.isNotEmpty) { + onSuccess(response); + } else { + onEmpty(); + } + } catch (e) { + if (onError != null) { + onError(e); + } else { + log.e("API call error: $e"); + } + } + } } diff --git a/lib/controller/task_planing/daily_task_planing_controller.dart b/lib/controller/task_planing/daily_task_planing_controller.dart new file mode 100644 index 0000000..9c998d8 --- /dev/null +++ b/lib/controller/task_planing/daily_task_planing_controller.dart @@ -0,0 +1,190 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/model/project_model.dart'; +import 'package:marco/model/dailyTaskPlaning/daily_task_planing_model.dart'; +import 'package:marco/model/employee_model.dart'; +import 'package:marco/helpers/widgets/my_form_validator.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; + +final Logger log = Logger(); + +class DailyTaskPlaningController extends GetxController { + List projects = []; + String? selectedProjectId; + List employees = []; + List dailyTasks = []; + RxMap uploadingStates = {}.obs; + MyFormValidator basicValidator = MyFormValidator(); + + List> roles = []; + RxnString selectedRoleId = RxnString(); + RxList selectedEmployees = [].obs; + + void updateSelectedEmployees() { + final selected = + employees.where((e) => uploadingStates[e.id]?.value == true).toList(); + selectedEmployees.value = selected; + } + + RxBool isLoading = false.obs; + @override + void onInit() { + super.onInit(); + fetchRoles(); + _initializeDefaults(); + } + + void _initializeDefaults() { + fetchProjects(); + } + + String? formFieldValidator(String? value, {required String fieldType}) { + if (value == null || value.trim().isEmpty) { + return 'This field is required'; + } + if (fieldType == "target") { + if (int.tryParse(value.trim()) == null) { + return 'Please enter a valid number'; + } + } + if (fieldType == "description") { + if (value.trim().length < 5) { + return 'Description must be at least 5 characters'; + } + } + return null; + } + + Future fetchRoles() async { + logger.i("Fetching roles..."); + final result = await ApiService.getRoles(); + if (result != null) { + roles = List>.from(result); + logger.i("Roles fetched successfully."); + update(); + } else { + logger.e("Failed to fetch roles."); + } + } + + void onRoleSelected(String? roleId) { + selectedRoleId.value = roleId; + logger.i("Role selected: $roleId"); + } + + Future assignDailyTask({ + required String workItemId, + required int plannedTask, + required String description, + required List taskTeam, + DateTime? assignmentDate, +}) async { + logger.i("Starting assign task..."); + + final response = await ApiService.assignDailyTask( + workItemId: workItemId, + plannedTask: plannedTask, + description: description, + taskTeam: taskTeam, + assignmentDate: assignmentDate, + ); + + if (response == true) { + logger.i("Task assigned successfully."); + showAppSnackbar( + title: "Success", + message: "Task assigned successfully!", + type: SnackbarType.success, + ); + return true; + } else { + logger.e("Failed to assign task."); + showAppSnackbar( + title: "Error", + message: "Failed to assign task.", + type: SnackbarType.error, + ); + return false; + } +} + + + Future fetchProjects() async { + try { + isLoading.value = true; + + final response = await ApiService.getProjects(); + if (response?.isEmpty ?? true) { + log.w("No project data found or API call failed."); + return; + } + + projects = response!.map((json) => ProjectModel.fromJson(json)).toList(); + selectedProjectId = projects.first.id.toString(); + log.i("Projects fetched: ${projects.length} projects loaded."); + update(); + + await fetchTaskData(selectedProjectId); + } catch (e, stack) { + log.e("Error fetching projects", error: e, stackTrace: stack); + } finally { + isLoading.value = false; + } + } + + Future fetchTaskData(String? projectId) async { + if (projectId == null) return; + + try { + isLoading.value = true; + + final response = await ApiService.getDailyTasksDetails(projectId); + if (response != null) { + final data = response['data']; + if (data != null) { + dailyTasks = [TaskPlanningDetailsModel.fromJson(data)]; + log.i("Daily task Planning Details fetched."); + } else { + log.e("Data field is null"); + } + } else { + log.e( + "Failed to fetch daily task planning Details for project $projectId"); + } + } catch (e, stack) { + log.e("Error fetching daily task data", error: e, stackTrace: stack); + } finally { + isLoading.value = false; + update(); + } + } + + Future fetchEmployeesByProject(String? projectId) async { + if (projectId == null || projectId.isEmpty) { + log.e("Project ID is required but was null or empty."); + return; + } + + isLoading.value = true; + try { + final response = await ApiService.getAllEmployeesByProject(projectId); + if (response != null && response.isNotEmpty) { + employees = + response.map((json) => EmployeeModel.fromJson(json)).toList(); + for (var emp in employees) { + uploadingStates[emp.id] = false.obs; + } + log.i("Employees fetched: ${employees.length} for project $projectId"); + } else { + log.w("No employees found for project $projectId."); + employees = []; + } + } catch (e) { + log.e("Error fetching employees for project $projectId: $e"); + } + + update(); + isLoading.value = false; + } +} diff --git a/lib/controller/task_planing/report_task_controller.dart b/lib/controller/task_planing/report_task_controller.dart index 379cd1d..05b0d03 100644 --- a/lib/controller/task_planing/report_task_controller.dart +++ b/lib/controller/task_planing/report_task_controller.dart @@ -5,9 +5,13 @@ import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:get/get.dart'; import 'package:logger/logger.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; +import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; final Logger logger = Logger(); + enum ApiStatus { idle, loading, success, failure } +final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController()); class ReportTaskController extends MyController { List files = []; MyFormValidator basicValidator = MyFormValidator(); @@ -96,18 +100,30 @@ class ReportTaskController extends MyController { basicValidator.getController('completed_work')?.text.trim(); if (completedWork == null || completedWork.isEmpty) { - Get.snackbar("Error", "Completed work is required."); + showAppSnackbar( + title: "Error", + message: "Completed work is required.", + type: SnackbarType.error, + ); return; } final completedWorkInt = int.tryParse(completedWork); if (completedWorkInt == null || completedWorkInt <= 0) { - Get.snackbar("Error", "Completed work must be a positive integer."); + showAppSnackbar( + title: "Error", + message: "Completed work must be a positive integer.", + type: SnackbarType.error, + ); return; } final commentField = basicValidator.getController('comment')?.text.trim(); if (commentField == null || commentField.isEmpty) { - Get.snackbar("Error", "Comment is required."); + showAppSnackbar( + title: "Error", + message: "Comment is required.", + type: SnackbarType.error, + ); return; } @@ -122,13 +138,25 @@ class ReportTaskController extends MyController { ); if (success) { - Get.snackbar("Success", "Task reported successfully!"); + showAppSnackbar( + title: "Success", + message: "Task reported successfully!", + type: SnackbarType.success, + ); } else { - Get.snackbar("Error", "Failed to report task."); + showAppSnackbar( + title: "Error", + message: "Failed to report task.", + type: SnackbarType.error, + ); } } catch (e) { logger.e("Error reporting task: $e"); - Get.snackbar("Error", "An error occurred while reporting the task."); + showAppSnackbar( + title: "Error", + message: "An error occurred while reporting the task.", + type: SnackbarType.error, + ); } finally { isLoading.value = false; } @@ -137,23 +165,17 @@ class ReportTaskController extends MyController { Future commentTask({ required String projectId, required String comment, - required int completedTask, - required List> checklist, - required DateTime reportedDate, }) async { - logger.i("Starting task report..."); + logger.i("Starting task comment..."); - final completedWork = - basicValidator.getController('completed_work')?.text.trim(); final commentField = basicValidator.getController('comment')?.text.trim(); - if (completedWork == null || completedWork.isEmpty) { - Get.snackbar("Error", "Completed work is required."); - return; - } - if (commentField == null || commentField.isEmpty) { - Get.snackbar("Error", "Comment is required."); + showAppSnackbar( + title: "Error", + message: "Comment is required.", + type: SnackbarType.error, + ); return; } @@ -166,13 +188,27 @@ class ReportTaskController extends MyController { ); if (success) { - Get.snackbar("Success", "Task commented successfully!"); + showAppSnackbar( + title: "Success", + message: "Task commented successfully!", + type: SnackbarType.success, + ); + + await taskController.fetchTaskData(projectId); } else { - Get.snackbar("Error", "Failed to comment task."); + showAppSnackbar( + title: "Error", + message: "Failed to comment task.", + type: SnackbarType.error, + ); } } catch (e) { logger.e("Error commenting task: $e"); - Get.snackbar("Error", "An error occurred while commenting the task."); + showAppSnackbar( + title: "Error", + message: "An error occurred while commenting the task.", + type: SnackbarType.error, + ); } finally { isLoading.value = false; } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 314ada1..09cb6c1 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -14,9 +14,12 @@ class ApiEndpoints { static const String getAllEmployees = "/employee/list"; static const String getRoles = "/roles/jobrole"; static const String createEmployee = "/employee/manage"; + static const String getEmployeeInfo = "/employee/profile/get"; // Daily Task Screen API Endpoints static const String getDailyTask = "/task/list"; - static const String reportTask = "/task/report"; - static const String commentTask = "task/comment"; + static const String reportTask = "/task/report"; + static const String commentTask = "/task/comment"; + static const String dailyTaskDetails = "/project/details"; + static const String assignDailyTask = "/task/assign"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 714b345..0508b81 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -7,6 +7,7 @@ import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:get/get.dart'; + final Logger logger = Logger(); class ApiService { @@ -46,6 +47,21 @@ class ApiService { return null; } + static dynamic _parseResponseForAllData(http.Response response, + {String label = ''}) { + _log("$label Response: ${response.body}"); + try { + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) { + return json; // 👈 Return full response, not just json['data'] + } + _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); + } catch (e) { + _log("Response parsing error [$label]: $e"); + } + return null; + } + static Future _getRequest(String endpoint, {Map? queryParams, bool hasRetried = false}) async { String? token = await _getToken(); @@ -291,6 +307,20 @@ class ApiService { } } + static Future?> getEmployeeDetails( + String employeeId) async { + final url = "${ApiEndpoints.getEmployeeInfo}/$employeeId"; + + final response = await _getRequest(url); + final data = response != null + ? _parseResponse(response, label: 'Employee Details') + : null; + if (data is Map) { + return data; + } + return null; + } + // ===== Daily Tasks API Calls ===== static Future?> getDailyTasks(String projectId, {DateTime? dateFrom, DateTime? dateTo}) async { @@ -339,6 +369,7 @@ class ApiService { return false; } } + static Future commentTask({ required String id, required String comment, @@ -359,11 +390,58 @@ class ApiService { final json = jsonDecode(response.body); if (response.statusCode == 200 && json['success'] == true) { - Get.back(); return true; } else { _log("Failed to comment task: ${json['message'] ?? 'Unknown error'}"); return false; } } + + // Daily Task Planing // + + static Future?> getDailyTasksDetails( + String projectId) async { + final url = "${ApiEndpoints.dailyTaskDetails}/$projectId"; + + final response = await _getRequest(url); + return response != null + ? _parseResponseForAllData(response, label: 'Daily Task Details') + as Map? + : null; + } + + static Future assignDailyTask({ + required String workItemId, + required int plannedTask, + required String description, + required List taskTeam, + DateTime? assignmentDate, + }) async { + final body = { + "workItemId": workItemId, + "plannedTask": plannedTask, + "description": description, + "taskTeam": taskTeam, + "assignmentDate": + (assignmentDate ?? DateTime.now()).toUtc().toIso8601String(), + }; + + final response = await _postRequest(ApiEndpoints.assignDailyTask, body); + + if (response == null) { + _log("Error: No response from server."); + return false; + } + + final json = jsonDecode(response.body); + + if (response.statusCode == 200 && json['success'] == true) { + Get.back(); + return true; + } else { + _log( + "Failed to assign daily task: ${json['message'] ?? 'Unknown error'}"); + return false; + } + } } diff --git a/lib/helpers/services/auth_service.dart b/lib/helpers/services/auth_service.dart index a60445e..0327d35 100644 --- a/lib/helpers/services/auth_service.dart +++ b/lib/helpers/services/auth_service.dart @@ -3,8 +3,7 @@ import 'package:http/http.dart' as http; import 'package:get/get.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/controller/permission_controller.dart'; -import 'package:logger/logger.dart'; // <-- Make sure this import is present - +import 'package:logger/logger.dart'; final Logger logger = Logger(); class AuthService { @@ -13,8 +12,6 @@ class AuthService { 'Content-Type': 'application/json', }; static bool isLoggedIn = false; - - /// Logs in the user and stores tokens if successful. static Future?> loginUser( Map data) async { try { @@ -44,7 +41,7 @@ class AuthService { Get.put(PermissionController()); - return null; // Success + return null; } else if (response.statusCode == 401) { return {"password": "Invalid email or password"}; } else { diff --git a/lib/helpers/widgets/my_image_compressor.dart b/lib/helpers/widgets/my_image_compressor.dart new file mode 100644 index 0000000..34f7e4d --- /dev/null +++ b/lib/helpers/widgets/my_image_compressor.dart @@ -0,0 +1,47 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; +import 'package:logger/logger.dart'; + +final logger = Logger(); + +Future compressImageToUnder100KB(File file) async { + int quality = 40; + Uint8List? result; + + const int maxWidth = 800; + const int maxHeight = 800; + + while (quality >= 10) { + result = await FlutterImageCompress.compressWithFile( + file.absolute.path, + quality: quality, + minWidth: maxWidth, + minHeight: maxHeight, + format: CompressFormat.jpeg, + ); + + if (result != null) { + logger.i('Quality: $quality, Size: ${(result.lengthInBytes / 1024).toStringAsFixed(2)} KB'); + + if (result.lengthInBytes <= 100 * 1024) { + return result; + } + } + + quality -= 10; + } + + return result; +} +Future saveCompressedImageToFile(Uint8List bytes) async { + final tempDir = await getTemporaryDirectory(); + final filePath = path.join( + tempDir.path, + 'compressed_${DateTime.now().millisecondsSinceEpoch}.jpg', + ); + final file = File(filePath); + return await file.writeAsBytes(bytes); +} diff --git a/lib/helpers/widgets/my_snackbar.dart b/lib/helpers/widgets/my_snackbar.dart new file mode 100644 index 0000000..cc57819 --- /dev/null +++ b/lib/helpers/widgets/my_snackbar.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +enum SnackbarType { success, error, warning, info } + +void showAppSnackbar({ + required String title, + required String message, + SnackbarType type = SnackbarType.info, +}) { + Color backgroundColor; + IconData iconData; + + switch (type) { + case SnackbarType.success: + backgroundColor = Colors.green; + iconData = Icons.check_circle; + break; + case SnackbarType.error: + backgroundColor = Colors.red; + iconData = Icons.error; + break; + case SnackbarType.warning: + backgroundColor = Colors.orange; + iconData = Icons.warning; + break; + case SnackbarType.info: + backgroundColor = Colors.blue; + iconData = Icons.info; + break; + } + + Get.snackbar( + title, + message, + backgroundColor: backgroundColor, + colorText: Colors.white, + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(16), + borderRadius: 8, + duration: const Duration(seconds: 3), + icon: Icon( + iconData, + color: Colors.white, + ), + ); +} diff --git a/lib/model/attendance/attendence_action_button.dart b/lib/model/attendance/attendence_action_button.dart index 9816237..eb0a1bc 100644 --- a/lib/model/attendance/attendence_action_button.dart +++ b/lib/model/attendance/attendence_action_button.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; - +import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/helpers/utils/attendance_actions.dart'; @@ -19,6 +19,98 @@ class AttendanceActionButton extends StatefulWidget { State createState() => _AttendanceActionButtonState(); } +Future _showCommentBottomSheet(BuildContext context, String actionText) async { + final TextEditingController commentController = TextEditingController(); + String? errorText; + + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + return Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 24, + bottom: MediaQuery.of(context).viewInsets.bottom + 24, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Add Comment for ${capitalizeFirstLetter(actionText)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 16), + TextField( + controller: commentController, + maxLines: 4, + decoration: InputDecoration( + hintText: 'Type your comment here...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade100, + errorText: errorText, + ), + onChanged: (_) { + if (errorText != null) { + setModalState(() => errorText = null); + } + }, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + final comment = commentController.text.trim(); + if (comment.isEmpty) { + setModalState(() { + errorText = 'Comment cannot be empty.'; + }); + return; + } + Navigator.of(context).pop(comment); + }, + child: const Text('Submit'), + ), + ), + ], + ), + ], + ), + ); + }, + ); + }, + ); +} + + +String capitalizeFirstLetter(String text) { + if (text.isEmpty) return text; + return text[0].toUpperCase() + text.substring(1); +} + class _AttendanceActionButtonState extends State { late final String uniqueLogKey; @@ -28,7 +120,6 @@ class _AttendanceActionButtonState extends State { uniqueLogKey = AttendanceButtonHelper.getUniqueKey( widget.employee.employeeId, widget.employee.id); - // Defer the Rx initialization after first frame to avoid setState during build WidgetsBinding.instance.addPostFrameCallback((_) { if (!widget.attendanceController.uploadingStates .containsKey(uniqueLogKey)) { @@ -58,9 +149,10 @@ class _AttendanceActionButtonState extends State { if (selectedDateTime.isAfter(checkInTime)) { return selectedDateTime; } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Please select a time after check-in time.")), + showAppSnackbar( + title: "Invalid Time", + message: "Please select a time after check-in time.", + type: SnackbarType.warning, ); return null; } @@ -69,14 +161,13 @@ class _AttendanceActionButtonState extends State { } void _handleButtonPressed(BuildContext context) async { - // Set uploading state true safely widget.attendanceController.uploadingStates[uniqueLogKey]?.value = true; if (widget.attendanceController.selectedProjectId == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Please select a project first"), - ), + showAppSnackbar( + title: "Project Required", + message: "Please select a project first", + type: SnackbarType.error, ); widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false; return; @@ -122,6 +213,12 @@ class _AttendanceActionButtonState extends State { break; } + final userComment = await _showCommentBottomSheet(context, actionText); + if (userComment == null || userComment.isEmpty) { + widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false; + return; + } + bool success = false; if (actionText == ButtonActions.requestRegularize) { final selectedTime = await showTimePickerForRegularization( @@ -135,37 +232,31 @@ class _AttendanceActionButtonState extends State { widget.employee.id, widget.employee.employeeId, widget.attendanceController.selectedProjectId!, - comment: actionText, + comment: userComment, action: updatedAction, imageCapture: imageCapture, markTime: formattedSelectedTime, ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success - ? '${actionText.toLowerCase()} marked successfully!' - : 'Failed to ${actionText.toLowerCase()}'), - ), - ); } } else { success = await widget.attendanceController.captureAndUploadAttendance( widget.employee.id, widget.employee.employeeId, widget.attendanceController.selectedProjectId!, - comment: actionText, + comment: userComment, action: updatedAction, imageCapture: imageCapture, ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success - ? '${actionText.toLowerCase()} marked successfully!' - : 'Failed to ${actionText.toLowerCase()}'), - ), - ); } + showAppSnackbar( + title: success ? '${capitalizeFirstLetter(actionText)} Success' : 'Error', + message: success + ? '${capitalizeFirstLetter(actionText)} marked successfully!' + : 'Failed to ${actionText.toLowerCase()}', + type: success ? SnackbarType.success : SnackbarType.error, + ); + widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false; if (success) { diff --git a/lib/model/attendance/attendence_filter_sheet.dart b/lib/model/attendance/attendence_filter_sheet.dart index ae28942..d00e273 100644 --- a/lib/model/attendance/attendence_filter_sheet.dart +++ b/lib/model/attendance/attendence_filter_sheet.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:marco/controller/permission_controller.dart'; import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:intl/intl.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; class AttendanceFilterBottomSheet extends StatelessWidget { final AttendanceController controller; @@ -23,7 +24,7 @@ class AttendanceFilterBottomSheet extends StatelessWidget { final end = DateFormat('dd MM yyyy').format(endDate); return "$start - $end"; } - return "Select Date Range"; + return "Date Range"; } @override @@ -72,16 +73,17 @@ class AttendanceFilterBottomSheet extends StatelessWidget { ) : null; - final selectedProjectName = - selectedProject?.name ?? "Select Project"; + final selectedProjectName = selectedProject?.name ?? "Select Project"; filterWidgets = [ - const Padding( - padding: EdgeInsets.fromLTRB(16, 12, 16, 4), + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Align( alignment: Alignment.centerLeft, - child: Text('Select Project', - style: TextStyle(fontWeight: FontWeight.bold)), + child: MyText.titleSmall( + "Project", + fontWeight: 600, + ), ), ), ListTile( @@ -92,18 +94,23 @@ class AttendanceFilterBottomSheet extends StatelessWidget { onTap: () => setState(() => showProjectList = true), ), const Divider(), - const Padding( - padding: EdgeInsets.fromLTRB(16, 12, 16, 4), + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Align( alignment: Alignment.centerLeft, - child: Text('Select View', - style: TextStyle(fontWeight: FontWeight.bold)), + child: MyText.titleSmall( + "View", + fontWeight: 600, + ), ), ), ...[ {'label': 'Today\'s Attendance', 'value': 'todaysAttendance'}, {'label': 'Attendance Logs', 'value': 'attendanceLogs'}, - {'label': 'Regularization Requests', 'value': 'regularizationRequests'}, + { + 'label': 'Regularization Requests', + 'value': 'regularizationRequests' + }, ].map((item) { return RadioListTile( dense: true, @@ -119,13 +126,13 @@ class AttendanceFilterBottomSheet extends StatelessWidget { if (tempSelectedTab == 'attendanceLogs') { filterWidgets.addAll([ const Divider(), - const Padding( - padding: EdgeInsets.fromLTRB(16, 12, 16, 4), + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Align( alignment: Alignment.centerLeft, - child: Text( - "Select Date Range", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + child: MyText.titleSmall( + "Date Range", + fontWeight: 600, ), ), ), @@ -139,7 +146,7 @@ class AttendanceFilterBottomSheet extends StatelessWidget { ), child: Ink( decoration: BoxDecoration( - color: Colors.grey.shade100, + color: const Color.fromARGB(255, 255, 255, 255), border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(10), ), @@ -147,7 +154,8 @@ class AttendanceFilterBottomSheet extends StatelessWidget { const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( children: [ - Icon(Icons.date_range, color: Colors.blue.shade600), + Icon(Icons.date_range, + color: const Color.fromARGB(255, 9, 9, 9)), const SizedBox(width: 12), Expanded( child: Text( @@ -160,7 +168,8 @@ class AttendanceFilterBottomSheet extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - const Icon(Icons.arrow_drop_down, color: Colors.grey), + const Icon(Icons.arrow_drop_down, + color: Color.fromARGB(255, 0, 0, 0)), ], ), ), @@ -178,14 +187,29 @@ class AttendanceFilterBottomSheet extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 8), + child: Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), ...filterWidgets, const Divider(), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( + backgroundColor: Color.fromARGB(255, 95, 132, 255), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), diff --git a/lib/model/attendance/regualrize_action_button.dart b/lib/model/attendance/regualrize_action_button.dart index b77b605..f77f5f7 100644 --- a/lib/model/attendance/regualrize_action_button.dart +++ b/lib/model/attendance/regualrize_action_button.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:marco/helpers/utils/attendance_actions.dart'; - +import 'package:marco/helpers/widgets/my_snackbar.dart'; enum ButtonActions { approve, reject } class RegularizeActionButton extends StatefulWidget { - final dynamic attendanceController; // Replace dynamic with your controller's type - final dynamic log; // Replace dynamic with your log model type + final dynamic + attendanceController; + final dynamic log; final String uniqueLogKey; final ButtonActions action; @@ -21,6 +22,11 @@ class RegularizeActionButton extends StatefulWidget { State createState() => _RegularizeActionButtonState(); } +String capitalizeFirstLetter(String text) { + if (text.isEmpty) return text; + return text[0].toUpperCase() + text.substring(1); +} + class _RegularizeActionButtonState extends State { bool isUploading = false; @@ -41,13 +47,16 @@ class _RegularizeActionButtonState extends State { Color get backgroundColor { // Use string keys for correct color lookup - return AttendanceActionColors.colors[_buttonTexts[widget.action]!] ?? Colors.grey; + return AttendanceActionColors.colors[_buttonTexts[widget.action]!] ?? + Colors.grey; } Future _handlePress() async { if (widget.attendanceController.selectedProjectId == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Please select a project first")), + showAppSnackbar( + title: 'Warning', + message: 'Please select a project first', + type: SnackbarType.warning, ); return; } @@ -56,9 +65,11 @@ class _RegularizeActionButtonState extends State { isUploading = true; }); - widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value = true; + widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value = + true; - final success = await widget.attendanceController.captureAndUploadAttendance( + final success = + await widget.attendanceController.captureAndUploadAttendance( widget.log.id, widget.log.employeeId, widget.attendanceController.selectedProjectId!, @@ -67,22 +78,27 @@ class _RegularizeActionButtonState extends State { imageCapture: false, ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success - ? '${_buttonTexts[widget.action]} marked successfully!' - : 'Failed to mark ${_buttonTexts[widget.action]}.'), - ), + showAppSnackbar( + title: success ? 'Success' : 'Error', + message: success + ? '${capitalizeFirstLetter(_buttonTexts[widget.action]!)} marked successfully!' + : 'Failed to mark ${capitalizeFirstLetter(_buttonTexts[widget.action]!)}.', + type: success ? SnackbarType.success : SnackbarType.error, ); if (success) { - widget.attendanceController.fetchEmployeesByProject(widget.attendanceController.selectedProjectId!); - widget.attendanceController.fetchAttendanceLogs(widget.attendanceController.selectedProjectId!); - await widget.attendanceController.fetchRegularizationLogs(widget.attendanceController.selectedProjectId!); - await widget.attendanceController.fetchProjectData(widget.attendanceController.selectedProjectId!); + widget.attendanceController.fetchEmployeesByProject( + widget.attendanceController.selectedProjectId!); + widget.attendanceController + .fetchAttendanceLogs(widget.attendanceController.selectedProjectId!); + await widget.attendanceController.fetchRegularizationLogs( + widget.attendanceController.selectedProjectId!); + await widget.attendanceController + .fetchProjectData(widget.attendanceController.selectedProjectId!); } - widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value = false; + widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value = + false; setState(() { isUploading = false; @@ -101,7 +117,8 @@ class _RegularizeActionButtonState extends State { onPressed: isUploading ? null : _handlePress, style: ElevatedButton.styleFrom( backgroundColor: backgroundColor, - foregroundColor: Colors.white, // Ensures visibility on all backgrounds + foregroundColor: + Colors.white, // Ensures visibility on all backgrounds padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), minimumSize: const Size(60, 20), textStyle: const TextStyle(fontSize: 12), diff --git a/lib/model/dailyTaskPlaning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlaning/assign_task_bottom_sheet .dart new file mode 100644 index 0000000..3b9cb2f --- /dev/null +++ b/lib/model/dailyTaskPlaning/assign_task_bottom_sheet .dart @@ -0,0 +1,402 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; +import 'package:marco/helpers/widgets/my_button.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; + +class AssignTaskBottomSheet extends StatefulWidget { + final String workLocation; + final String activityName; + final int pendingTask; + final String workItemId; + final DateTime assignmentDate; + final String buildingName; + final String floorName; + final String workAreaName; + + const AssignTaskBottomSheet({ + super.key, + required this.buildingName, + required this.workLocation, + required this.floorName, + required this.workAreaName, + required this.activityName, + required this.pendingTask, + required this.workItemId, + required this.assignmentDate, + }); + + @override + State createState() => _AssignTaskBottomSheetState(); +} + +class _AssignTaskBottomSheetState extends State { + final DailyTaskPlaningController controller = Get.find(); + final TextEditingController targetController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + String? selectedProjectId; + + final ScrollController _employeeListScrollController = ScrollController(); + + @override + void dispose() { + _employeeListScrollController.dispose(); + targetController.dispose(); + descriptionController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + selectedProjectId = controller.selectedProjectId; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (selectedProjectId != null) { + controller.fetchEmployeesByProject(selectedProjectId!); + } + }); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Container( + padding: MediaQuery.of(context).viewInsets.add(MySpacing.all(16)), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(Icons.assignment, color: Colors.black54), + SizedBox(width: 8), + MyText.titleMedium("Assign Task", + fontSize: 18, fontWeight: 600), + ], + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Get.back(), + ), + ], + ), + Divider(), + _infoRow(Icons.location_on, "Work Location", + "${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}"), + Divider(), + _infoRow(Icons.pending_actions, "Pending Task of Activity", + "${widget.pendingTask}"), + Divider(), + GestureDetector( + onTap: () { + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + final Size screenSize = overlay.size; + + showMenu( + context: context, + position: RelativeRect.fromLTRB( + screenSize.width / 2 - 100, + screenSize.height / 2 - 20, + screenSize.width / 2 - 100, + screenSize.height / 2 - 20, + ), + items: [ + const PopupMenuItem( + value: 'all', + child: Text("All Roles"), + ), + ...controller.roles.map((role) { + return PopupMenuItem( + value: role['id'].toString(), + child: Text(role['name'] ?? 'Unknown Role'), + ); + }), + ], + ).then((value) { + if (value != null) { + controller.onRoleSelected(value == 'all' ? null : value); + } + }); + }, + child: Row( + children: [ + MyText.titleMedium("Select Team :", fontWeight: 600), + const SizedBox(width: 4), + Icon(Icons.filter_alt, + color: const Color.fromARGB(255, 95, 132, 255)), + ], + ), + ), + MySpacing.height(8), + Container( + constraints: BoxConstraints( + maxHeight: 150, + ), + child: _buildEmployeeList(), + ), + MySpacing.height(8), + Obx(() { + if (controller.selectedEmployees.isEmpty) { + return Container(); + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Wrap( + spacing: 4, + runSpacing: 4, + children: controller.selectedEmployees.map((e) { + return Obx(() { + final isSelected = + controller.uploadingStates[e.id]?.value ?? false; + if (!isSelected) return Container(); + + return Chip( + label: Text(e.name, + style: const TextStyle(color: Colors.white)), + backgroundColor: + const Color.fromARGB(255, 95, 132, 255), + deleteIcon: + const Icon(Icons.close, color: Colors.white), + onDeleted: () { + controller.uploadingStates[e.id]?.value = false; + controller.updateSelectedEmployees(); + }); + }); + }).toList(), + ), + ); + }), + _buildTextField( + icon: Icons.track_changes, + label: "Target for Today :", + controller: targetController, + hintText: "Enter target", + keyboardType: TextInputType.number, + validatorType: "target", + ), + MySpacing.height(24), + _buildTextField( + icon: Icons.description, + label: "Description :", + controller: descriptionController, + hintText: "Enter task description", + maxLines: 3, + validatorType: "description", + ), + MySpacing.height(24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyButton( + onPressed: _onAssignTaskPressed, + backgroundColor: const Color.fromARGB(255, 95, 132, 255), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + MyText.bodyMedium("Assign Task", color: Colors.white), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildEmployeeList() { + return Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + final selectedRoleId = controller.selectedRoleId.value; + + final filteredEmployees = selectedRoleId == null + ? controller.employees + : controller.employees + .where((e) => e.jobRoleID.toString() == selectedRoleId) + .toList(); + + if (filteredEmployees.isEmpty) { + return const Text("No employees found for selected role."); + } + + return Scrollbar( + controller: _employeeListScrollController, + thumbVisibility: true, + interactive: true, + child: ListView.builder( + controller: _employeeListScrollController, + shrinkWrap: true, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: filteredEmployees.length, + itemBuilder: (context, index) { + final employee = filteredEmployees[index]; + final rxBool = controller.uploadingStates[employee.id]; + return Obx(() => Padding( + padding: const EdgeInsets.symmetric(vertical: 0), + child: Row( + children: [ + Theme( + data: Theme.of(context) + .copyWith(unselectedWidgetColor: Colors.black), + child: Checkbox( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + side: const BorderSide(color: Colors.black), + ), + value: rxBool?.value ?? false, + onChanged: (bool? selected) { + if (rxBool != null) { + rxBool.value = selected ?? false; + controller.updateSelectedEmployees(); + } + }, + fillColor: + WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return const Color.fromARGB(255, 95, 132, 255); + } + return Colors.transparent; + }), + checkColor: Colors.white, + side: const BorderSide(color: Colors.black), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text(employee.name, + style: TextStyle(fontSize: 14))), + ], + ), + )); + }, + ), + ); + }); + } + + Widget _buildTextField({ + required IconData icon, + required String label, + required TextEditingController controller, + required String hintText, + TextInputType keyboardType = TextInputType.text, + int maxLines = 1, + required String validatorType, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 18, color: Colors.black54), + const SizedBox(width: 6), + MyText.titleMedium(label, fontWeight: 600), + ], + ), + MySpacing.height(6), + TextFormField( + controller: controller, + keyboardType: keyboardType, + maxLines: maxLines, + decoration: InputDecoration( + hintText: hintText, + border: const OutlineInputBorder(), + ), + validator: (value) => this + .controller + .formFieldValidator(value, fieldType: validatorType), + ), + ], + ); + } + + Widget _infoRow(IconData icon, String title, String value) { + return Padding( + padding: MySpacing.y(6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 20, color: Colors.grey[700]), + const SizedBox(width: 8), + Expanded( + child: RichText( + text: TextSpan( + children: [ + WidgetSpan( + child: MyText.titleMedium("$title: ", + fontWeight: 600, color: Colors.black), + ), + TextSpan( + text: value, + style: const TextStyle(color: Colors.black), + ), + ], + ), + ), + ), + ], + ), + ); + } + + void _onAssignTaskPressed() { + final selectedTeam = controller.uploadingStates.entries + .where((e) => e.value.value) + .map((e) => e.key) + .toList(); + + if (selectedTeam.isEmpty) { + showAppSnackbar( + title: "Team Required", + message: "Please select at least one team member", + type: SnackbarType.error, + ); + return; + } + + final target = int.tryParse(targetController.text.trim()); + if (target == null || target <= 0) { + showAppSnackbar( + title: "Invalid Input", + message: "Please enter a valid target number", + type: SnackbarType.error, + ); + return; + } + + final description = descriptionController.text.trim(); + if (description.isEmpty) { + showAppSnackbar( + title: "Description Required", + message: "Please enter a description", + type: SnackbarType.error, + ); + return; + } + + controller.assignDailyTask( + workItemId: widget.workItemId, + plannedTask: target, + description: description, + taskTeam: selectedTeam, + assignmentDate: widget.assignmentDate, + ); + } +} diff --git a/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart b/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart new file mode 100644 index 0000000..39b0e48 --- /dev/null +++ b/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart @@ -0,0 +1,482 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/task_planing/report_task_controller.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/my_button.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_text_style.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:marco/helpers/widgets/my_team_model_sheet.dart'; +import 'package:intl/intl.dart'; + +class CommentTaskBottomSheet extends StatefulWidget { + final Map taskData; + final VoidCallback? onCommentSuccess; + + const CommentTaskBottomSheet({ + super.key, + required this.taskData, + this.onCommentSuccess, + }); + + @override + State createState() => _CommentTaskBottomSheetState(); +} + +class _Member { + final String firstName; + _Member(this.firstName); +} + +class _CommentTaskBottomSheetState extends State + with UIMixin { + late ReportTaskController controller; + final ScrollController _scrollController = ScrollController(); + @override + void initState() { + super.initState(); + controller = Get.put(ReportTaskController(), + tag: widget.taskData['taskId'] ?? UniqueKey().toString()); + final data = widget.taskData; + controller.basicValidator.getController('assigned_date')?.text = + data['assignedOn'] ?? ''; + controller.basicValidator.getController('assigned_by')?.text = + data['assignedBy'] ?? ''; + controller.basicValidator.getController('work_area')?.text = + data['location'] ?? ''; + controller.basicValidator.getController('activity')?.text = + data['activity'] ?? ''; + controller.basicValidator.getController('planned_work')?.text = + data['plannedWork'] ?? ''; + controller.basicValidator.getController('completed_work')?.text = + data['completedWork'] ?? ''; + controller.basicValidator.getController('team_members')?.text = + (data['teamMembers'] as List).join(', '); + controller.basicValidator.getController('assigned')?.text = + data['assigned'] ?? ''; + controller.basicValidator.getController('task_id')?.text = + data['taskId'] ?? ''; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + } + }); + } + + String timeAgo(String dateString) { + try { + DateTime date = DateTime.parse(dateString + "Z").toLocal(); + final now = DateTime.now(); + final difference = now.difference(date); + if (difference.inDays > 8) { + return DateFormat('dd-MM-yyyy').format(date); + } else if (difference.inDays >= 1) { + return '${difference.inDays} day${difference.inDays > 1 ? 's' : ''} ago'; + } else if (difference.inHours >= 1) { + return '${difference.inHours} hr${difference.inHours > 1 ? 's' : ''} ago'; + } else if (difference.inMinutes >= 1) { + return '${difference.inMinutes} min${difference.inMinutes > 1 ? 's' : ''} ago'; + } else { + return 'just now'; + } + } catch (e) { + print('Error parsing date: $e'); + return ''; + } + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom + 24, + left: 24, + right: 24, + top: 12, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Drag handle + Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(2), + ), + ), + GetBuilder( + tag: widget.taskData['taskId'] ?? '', + builder: (controller) { + return Form( + key: controller.basicValidator.formKey, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MyText.titleMedium( + "Comment Task", + fontWeight: 600, + fontSize: 18, + ), + ], + ), + MySpacing.height(24), + buildRow( + "Assigned By", + controller.basicValidator + .getController('assigned_by') + ?.text + .trim(), + icon: Icons.person_outline, + ), + buildRow( + "Work Area", + controller.basicValidator + .getController('work_area') + ?.text + .trim(), + icon: Icons.place_outlined, + ), + buildRow( + "Activity", + controller.basicValidator + .getController('activity') + ?.text + .trim(), + icon: Icons.assignment_outlined, + ), + buildRow( + "Planned Work", + controller.basicValidator + .getController('planned_work') + ?.text + .trim(), + icon: Icons.schedule_outlined, + ), + buildRow( + "Completed Work", + controller.basicValidator + .getController('completed_work') + ?.text + .trim(), + icon: Icons.done_all_outlined, + ), + buildTeamMembers(), + Row( + children: [ + Icon(Icons.comment_outlined, + size: 18, color: Colors.grey[700]), + MySpacing.width(8), + MyText.titleSmall( + "Comment:", + fontWeight: 600, + ), + ], + ), + MySpacing.height(8), + TextFormField( + validator: controller.basicValidator + .getValidation('comment'), + controller: controller.basicValidator + .getController('comment'), + keyboardType: TextInputType.text, + decoration: InputDecoration( + hintText: "eg: Work done successfully", + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(16), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + ), + MySpacing.height(24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyButton.text( + onPressed: () => Navigator.of(context).pop(), + padding: MySpacing.xy(20, 16), + splashColor: contentTheme.secondary.withAlpha(25), + child: MyText.bodySmall('Cancel'), + ), + MySpacing.width(12), + Obx(() { + return MyButton( + onPressed: controller.isLoading.value + ? null + : () async { + if (controller.basicValidator + .validateForm()) { + await controller.commentTask( + projectId: controller.basicValidator + .getController('task_id') + ?.text ?? + '', + comment: controller.basicValidator + .getController('comment') + ?.text ?? + '', + ); + if (widget.onCommentSuccess != null) { + widget.onCommentSuccess!(); + } + } + }, + elevation: 0, + padding: MySpacing.xy(20, 16), + backgroundColor: Colors.blueAccent, + borderRadiusAll: AppStyle.buttonRadius.medium, + child: controller.isLoading.value + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation( + contentTheme.onPrimary), + ), + ) + : MyText.bodySmall( + 'Comment', + color: contentTheme.onPrimary, + ), + ); + }), + ], + ), + MySpacing.height(24), + if ((widget.taskData['taskComments'] as List?) + ?.isNotEmpty == + true) ...[ + Row( + children: [ + Icon(Icons.chat_bubble_outline, + size: 18, color: Colors.grey[700]), + MySpacing.width(8), + MyText.titleSmall( + "Comments", + fontWeight: 600, + ), + ], + ), + MySpacing.height(12), + Builder( + builder: (context) { + final comments = List>.from( + widget.taskData['taskComments'] as List, + ); + + comments.sort((a, b) { + final aDate = + DateTime.tryParse(a['date'] ?? '') ?? + DateTime.fromMillisecondsSinceEpoch(0); + final bDate = + DateTime.tryParse(b['date'] ?? '') ?? + DateTime.fromMillisecondsSinceEpoch(0); + return bDate.compareTo( + aDate); // descending: newest first + }); + + return SizedBox( + height: 300, + child: ListView.builder( + itemCount: comments.length, + itemBuilder: (context, index) { + final comment = comments[index]; + final commentText = comment['text'] ?? '-'; + final commentedBy = + comment['commentedBy'] ?? 'Unknown'; + final relativeTime = + timeAgo(comment['date'] ?? ''); + + return Container( + margin: EdgeInsets.symmetric( + vertical: 6, horizontal: 8), + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // Avatar for commenter + Avatar( + firstName: + commentedBy.split(' ').first, + lastName: commentedBy + .split(' ') + .length > + 1 + ? commentedBy.split(' ').last + : '', + size: 32, + ), + SizedBox(width: 12), + // Comment text and meta + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + commentedBy, + style: TextStyle( + fontWeight: + FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + relativeTime, + style: TextStyle( + fontSize: 12, + color: Colors.black54, + ), + ) + ], + ), + SizedBox(height: 6), + Text( + commentText, + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + ); + }, + ), + ], + ], + ), + ), + ); + }, + ), + ], + ), + ), + ); + } + + Widget buildTeamMembers() { + final teamMembersText = + controller.basicValidator.getController('team_members')?.text ?? ''; + final members = teamMembersText + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + MyText.titleSmall( + "Team Members:", + fontWeight: 600, + ), + MySpacing.width(12), + GestureDetector( + onTap: () { + TeamBottomSheet.show( + context: context, + teamMembers: members.map((name) => _Member(name)).toList(), + ); + }, + child: SizedBox( + height: 32, + width: 100, + child: Stack( + children: [ + for (int i = 0; i < members.length.clamp(0, 3); i++) + Positioned( + left: i * 24.0, + child: Tooltip( + message: members[i], + child: Avatar( + firstName: members[i], + lastName: '', + size: 32, + ), + ), + ), + if (members.length > 3) + Positioned( + left: 2 * 24.0, + child: CircleAvatar( + radius: 16, + backgroundColor: Colors.grey.shade300, + child: MyText.bodyMedium( + '+${members.length - 3}', + style: const TextStyle( + fontSize: 12, color: Colors.black87), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget buildRow(String label, String? value, {IconData? icon}) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.only(right: 8.0, top: 2), + child: Icon(icon, size: 18, color: Colors.grey[700]), + ), + MyText.titleSmall( + "$label:", + fontWeight: 600, + ), + MySpacing.width(12), + Expanded( + child: MyText.bodyMedium(value?.isNotEmpty == true ? value! : "-"), + ), + ], + ), + ); + } +} diff --git a/lib/model/dailyTaskPlaning/daily_progress_report_filter.dart b/lib/model/dailyTaskPlaning/daily_progress_report_filter.dart new file mode 100644 index 0000000..8012977 --- /dev/null +++ b/lib/model/dailyTaskPlaning/daily_progress_report_filter.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/controller/dashboard/daily_task_controller.dart'; +import 'package:intl/intl.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +class DailyProgressReportFilter extends StatefulWidget { + final DailyTaskController controller; + final PermissionController permissionController; + + const DailyProgressReportFilter({ + super.key, + required this.controller, + required this.permissionController, + }); + + @override + State createState() => + _DailyProgressReportFilterState(); +} + +class _DailyProgressReportFilterState extends State { + late String? tempSelectedProjectId; + bool showProjectList = false; + + @override + void initState() { + super.initState(); + tempSelectedProjectId = widget.controller.selectedProjectId; + } + + String getLabelText() { + final startDate = widget.controller.startDateTask; + final endDate = widget.controller.endDateTask; + if (startDate != null && endDate != null) { + final start = DateFormat('dd MM yyyy').format(startDate); + final end = DateFormat('dd MM yyyy').format(endDate); + return "$start - $end"; + } + return "Select Date Range"; + } + + @override + Widget build(BuildContext context) { + final accessibleProjects = widget.controller.projects + .where((project) => widget.permissionController + .isUserAssignedToProject(project.id.toString())) + .toList(); + + List filterWidgets; + + if (showProjectList) { + filterWidgets = accessibleProjects.isEmpty + ? [ + const Padding( + padding: EdgeInsets.all(12.0), + child: Center(child: Text('No Projects Assigned')), + ), + ] + : accessibleProjects.map((project) { + final isSelected = tempSelectedProjectId == project.id.toString(); + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + title: Text(project.name), + trailing: isSelected ? const Icon(Icons.check) : null, + onTap: () { + setState(() { + tempSelectedProjectId = project.id.toString(); + showProjectList = false; + }); + }, + ); + }).toList(); + } else { + final selectedProject = accessibleProjects.isNotEmpty + ? accessibleProjects.firstWhere( + (p) => p.id.toString() == tempSelectedProjectId, + orElse: () => accessibleProjects[0], + ) + : null; + + final selectedProjectName = selectedProject?.name ?? "Select Project"; + + filterWidgets = [ + Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Align( + alignment: Alignment.centerLeft, + child: MyText.titleSmall( + 'Select Project', + fontWeight: 600, + ), + ), + ), + ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + title: Text(selectedProjectName), + trailing: const Icon(Icons.arrow_drop_down), + onTap: () => setState(() => showProjectList = true), + ), + ]; + + filterWidgets.addAll([ + const Divider(), + Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Align( + alignment: Alignment.centerLeft, + child: MyText.titleSmall( + "Select Date Range", + fontWeight: 600, + )), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () => widget.controller.selectDateRangeForTaskData( + context, + widget.controller, + ), + child: Ink( + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + Icon(Icons.date_range, color: Colors.blue.shade600), + const SizedBox(width: 12), + Expanded( + child: Text( + getLabelText(), + style: const TextStyle( + fontSize: 16, + color: Colors.black87, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), + ), + ), + ), + ]); + } + + return SafeArea( + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 8), + child: Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ...filterWidgets, + const Divider(), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('Apply Filter'), + onPressed: () { + Navigator.pop(context, { + 'projectId': tempSelectedProjectId, + }); + }, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/model/dailyTaskPlaning/daily_task_planing_filter.dart b/lib/model/dailyTaskPlaning/daily_task_planing_filter.dart new file mode 100644 index 0000000..815b788 --- /dev/null +++ b/lib/model/dailyTaskPlaning/daily_task_planing_filter.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +class DailyTaskPlaningFilter extends StatelessWidget { + final DailyTaskPlaningController controller; + final PermissionController permissionController; + + const DailyTaskPlaningFilter({ + super.key, + required this.controller, + required this.permissionController, + }); + + @override + Widget build(BuildContext context) { + String? tempSelectedProjectId = controller.selectedProjectId; + bool showProjectList = false; + + final accessibleProjects = controller.projects + .where((project) => + permissionController.isUserAssignedToProject(project.id.toString())) + .toList(); + + return StatefulBuilder(builder: (context, setState) { + List filterWidgets; + + if (showProjectList) { + filterWidgets = accessibleProjects.isEmpty + ? [ + Padding( + padding: EdgeInsets.all(12.0), + child: Center( + child: MyText.titleSmall( + 'No Projects Assigned', + fontWeight: 600, + ), + ), + ), + ] + : accessibleProjects.map((project) { + final isSelected = + tempSelectedProjectId == project.id.toString(); + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + title: MyText.titleSmall(project.name), + trailing: isSelected ? const Icon(Icons.check) : null, + onTap: () { + setState(() { + tempSelectedProjectId = project.id.toString(); + showProjectList = false; + }); + }, + ); + }).toList(); + } else { + final selectedProject = accessibleProjects.isNotEmpty + ? accessibleProjects.firstWhere( + (p) => p.id.toString() == tempSelectedProjectId, + orElse: () => accessibleProjects[0], + ) + : null; + + final selectedProjectName = selectedProject?.name ?? "Select Project"; + + filterWidgets = [ + Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Align( + alignment: Alignment.centerLeft, + child: MyText.titleSmall( + 'Select Project', + fontWeight: 600, + ), + ), + ), + ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + title: MyText.titleSmall(selectedProjectName), + trailing: const Icon(Icons.arrow_drop_down), + onTap: () => setState(() => showProjectList = true), + ), + ]; + } + + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 8), + child: Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ...filterWidgets, + const Divider(), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Color.fromARGB(255, 95, 132, 255), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: MyText.titleSmall( + 'Apply Filter', + fontWeight: 600, + color: Colors.white, + ), + onPressed: () { + Navigator.pop(context, { + 'projectId': tempSelectedProjectId, + }); + }, + ), + ), + ), + ], + ), + ), + ); + }); + } +} diff --git a/lib/model/dailyTaskPlaning/daily_task_planing_model.dart b/lib/model/dailyTaskPlaning/daily_task_planing_model.dart new file mode 100644 index 0000000..da2085b --- /dev/null +++ b/lib/model/dailyTaskPlaning/daily_task_planing_model.dart @@ -0,0 +1,234 @@ +class TaskPlanningDetailsModel { + final List buildings; + final String id; + final String name; + final String projectAddress; + final String contactPerson; + final DateTime startDate; + final DateTime endDate; + final String projectStatusId; + + TaskPlanningDetailsModel({ + required this.buildings, + required this.id, + required this.name, + required this.projectAddress, + required this.contactPerson, + required this.startDate, + required this.endDate, + required this.projectStatusId, + }); + + factory TaskPlanningDetailsModel.fromJson(Map json) { + return TaskPlanningDetailsModel( + buildings: (json['buildings'] as List?) + ?.map((b) => Building.fromJson(b)) + .toList() ?? + [], + id: json['id'], + name: json['name'], + projectAddress: json['projectAddress'], + contactPerson: json['contactPerson'], + startDate: DateTime.parse(json['startDate']), + endDate: DateTime.parse(json['endDate']), + projectStatusId: json['projectStatusId'], + ); + } +} + +class Building { + final String id; + final String name; + final String description; + final List floors; + + Building({ + required this.id, + required this.name, + required this.description, + required this.floors, + }); + + factory Building.fromJson(Map json) { + return Building( + id: json['id'], + name: json['name'], + description: json['description'], + floors: (json['floors'] as List).map((f) => Floor.fromJson(f)).toList(), + ); + } +} + +class Floor { + final String id; + final String floorName; + final List workAreas; + + Floor({ + required this.id, + required this.floorName, + required this.workAreas, + }); + + factory Floor.fromJson(Map json) { + return Floor( + id: json['id'], + floorName: json['floorName'], + workAreas: + (json['workAreas'] as List).map((w) => WorkArea.fromJson(w)).toList(), + ); + } +} + +class WorkArea { + final String id; + final String areaName; + final List workItems; + + WorkArea({ + required this.id, + required this.areaName, + required this.workItems, + }); + + factory WorkArea.fromJson(Map json) { + return WorkArea( + id: json['id'], + areaName: json['areaName'], + workItems: (json['workItems'] as List) + .map((w) => WorkItemWrapper.fromJson(w)) + .toList(), + ); + } +} + +class WorkItemWrapper { + final String workItemId; + final WorkItem workItem; + + WorkItemWrapper({ + required this.workItemId, + required this.workItem, + }); + + factory WorkItemWrapper.fromJson(Map json) { + return WorkItemWrapper( + workItemId: json['workItemId'], + workItem: WorkItem.fromJson(json['workItem']), + ); + } +} + +class WorkItem { + final String? id; + final String? activityId; + final String? workCategoryId; + final String? workAreaId; + final WorkAreaBasic? workArea; + final ActivityMaster? activityMaster; + final WorkCategoryMaster? workCategoryMaster; + final int? plannedWork; + final int? completedWork; + final DateTime? taskDate; + final String? tenantId; + final Tenant? tenant; + + WorkItem({ + this.id, + this.activityId, + this.workCategoryId, + this.workAreaId, + this.workArea, + this.activityMaster, + this.workCategoryMaster, + this.plannedWork, + this.completedWork, + this.taskDate, + this.tenantId, + this.tenant, + }); + + factory WorkItem.fromJson(Map json) { + return WorkItem( + id: json['id'] as String?, + activityId: json['activityId'] as String?, + workCategoryId: json['workCategoryId'] as String?, + workAreaId: json['workAreaId'] as String?, + workArea: json['workArea'] != null + ? WorkAreaBasic.fromJson(json['workArea'] as Map) + : null, + activityMaster: json['activityMaster'] != null + ? ActivityMaster.fromJson( + json['activityMaster'] as Map) + : null, + workCategoryMaster: json['workCategoryMaster'] != null + ? WorkCategoryMaster.fromJson( + json['workCategoryMaster'] as Map) + : null, + plannedWork: json['plannedWork'] as int?, + completedWork: json['completedWork'] as int?, + taskDate: + json['taskDate'] != null ? DateTime.tryParse(json['taskDate']) : null, + tenantId: json['tenantId'] as String?, + tenant: json['tenant'] != null + ? Tenant.fromJson(json['tenant'] as Map) + : null, + ); + } +} + +class WorkAreaBasic { + final String? id; + final String? name; + + WorkAreaBasic({this.id, this.name}); + + factory WorkAreaBasic.fromJson(Map json) { + return WorkAreaBasic( + id: json['id'] as String?, + name: json['name'] as String?, + ); + } +} + +class ActivityMaster { + final String? id; + final String? name; + + ActivityMaster({this.id, this.name}); + + factory ActivityMaster.fromJson(Map json) { + return ActivityMaster( + id: json['id'] as String?, + name: json['activityName'] as String?, + ); + } +} + +class WorkCategoryMaster { + final String? id; + final String? name; + + WorkCategoryMaster({this.id, this.name}); + + factory WorkCategoryMaster.fromJson(Map json) { + return WorkCategoryMaster( + id: json['id'] as String?, + name: json['name'] as String?, + ); + } +} + +class Tenant { + final String? id; + final String? name; + + Tenant({this.id, this.name}); + + factory Tenant.fromJson(Map json) { + return Tenant( + id: json['id'] as String?, + name: json['name'] as String?, + ); + } +} diff --git a/lib/model/dailyTaskPlaning/report_task_bottom_sheet.dart b/lib/model/dailyTaskPlaning/report_task_bottom_sheet.dart new file mode 100644 index 0000000..134fd85 --- /dev/null +++ b/lib/model/dailyTaskPlaning/report_task_bottom_sheet.dart @@ -0,0 +1,309 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/task_planing/report_task_controller.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/my_button.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_text_style.dart'; + +class ReportTaskBottomSheet extends StatefulWidget { + final Map taskData; + + const ReportTaskBottomSheet({super.key, required this.taskData}); + + @override + State createState() => _ReportTaskBottomSheetState(); +} + +class _ReportTaskBottomSheetState extends State + with UIMixin { + late final ReportTaskController controller; + + @override + void initState() { + super.initState(); + // Initialize the controller with a unique tag (optional) + controller = Get.put(ReportTaskController(), + tag: widget.taskData['taskId'] ?? UniqueKey().toString()); + + final taskData = widget.taskData; + controller.basicValidator.getController('assigned_date')?.text = + taskData['assignedOn'] ?? ''; + controller.basicValidator.getController('assigned_by')?.text = + taskData['assignedBy'] ?? ''; + controller.basicValidator.getController('work_area')?.text = + taskData['location'] ?? ''; + controller.basicValidator.getController('activity')?.text = + taskData['activity'] ?? ''; + controller.basicValidator.getController('team_size')?.text = + taskData['teamSize']?.toString() ?? ''; + controller.basicValidator.getController('assigned')?.text = + taskData['assigned'] ?? ''; + controller.basicValidator.getController('task_id')?.text = + taskData['taskId'] ?? ''; + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom + 24, + left: 24, + right: 24, + top: 12, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Drag handle + Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(2), + ), + ), + GetBuilder( + tag: widget.taskData['taskId'] ?? '', + init: controller, + builder: (_) { + return Form( + key: controller.basicValidator.formKey, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: MyText.titleMedium( + "Report Task", + fontWeight: 600, + ), + ), + MySpacing.height(16), + buildRow( + "Assigned Date", + controller.basicValidator + .getController('assigned_date') + ?.text + .trim()), + buildRow( + "Assigned By", + controller.basicValidator + .getController('assigned_by') + ?.text + .trim()), + buildRow( + "Work Area", + controller.basicValidator + .getController('work_area') + ?.text + .trim()), + buildRow( + "Activity", + controller.basicValidator + .getController('activity') + ?.text + .trim()), + buildRow( + "Team Size", + controller.basicValidator + .getController('team_size') + ?.text + .trim()), + buildRow( + "Assigned", + controller.basicValidator + .getController('assigned') + ?.text + .trim()), + Row( + children: [ + Icon(Icons.work_outline, + size: 18, color: Colors.grey[700]), + MySpacing.width(8), + MyText.titleSmall( + "Completed Work:", + fontWeight: 600, + ), + ], + ), + MySpacing.height(8), + TextFormField( + validator: controller.basicValidator + .getValidation('completed_work'), + controller: controller.basicValidator + .getController('completed_work'), + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: "eg: 10", + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(16), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + ), + MySpacing.height(24), + Row( + children: [ + Icon(Icons.comment_outlined, + size: 18, color: Colors.grey[700]), + MySpacing.width(8), + MyText.titleSmall( + "Comment:", + fontWeight: 600, + ), + ], + ), + MySpacing.height(8), + TextFormField( + validator: controller.basicValidator + .getValidation('comment'), + controller: controller.basicValidator + .getController('comment'), + keyboardType: TextInputType.text, + decoration: InputDecoration( + hintText: "eg: Work done successfully", + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(16), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + ), + MySpacing.height(24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyButton.text( + onPressed: () => Navigator.of(context).pop(), + padding: MySpacing.xy(20, 16), + splashColor: contentTheme.secondary.withAlpha(25), + child: MyText.bodySmall('Cancel'), + ), + MySpacing.width(12), + Obx(() { + return MyButton( + onPressed: controller.reportStatus.value == + ApiStatus.loading + ? null + : () async { + if (controller.basicValidator + .validateForm()) { + await controller.reportTask( + projectId: controller.basicValidator + .getController('task_id') + ?.text ?? + '', + comment: controller.basicValidator + .getController('comment') + ?.text ?? + '', + completedTask: int.tryParse( + controller.basicValidator + .getController( + 'completed_work') + ?.text ?? + '') ?? + 0, + checklist: [], + reportedDate: DateTime.now(), + + ); + } + }, + elevation: 0, + padding: MySpacing.xy(20, 16), + backgroundColor: Colors.blueAccent, + borderRadiusAll: AppStyle.buttonRadius.medium, + child: controller.reportStatus.value == + ApiStatus.loading + ? SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation( + contentTheme.onPrimary), + ), + ) + : MyText.bodySmall( + 'Report', + color: contentTheme.onPrimary, + ), + ); + }), + ], + ), + ], + ), + ), + ); + }, + ), + ], + ), + ), + ); + } + + Widget buildRow(String label, String? value) { + IconData icon; + switch (label) { + case "Assigned Date": + icon = Icons.calendar_today_outlined; + break; + case "Assigned By": + icon = Icons.person_outline; + break; + case "Work Area": + icon = Icons.place_outlined; + break; + case "Activity": + icon = Icons.run_circle_outlined; + break; + case "Team Size": + icon = Icons.group_outlined; + break; + case "Assigned": + icon = Icons.assignment_turned_in_outlined; + break; + default: + icon = Icons.info_outline; + } + + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 18, color: Colors.grey[700]), + MySpacing.width(8), + MyText.titleSmall( + "$label:", + fontWeight: 600, + ), + MySpacing.width(12), + Expanded( + child: MyText.bodyMedium(value?.isNotEmpty == true ? value! : "-"), + ), + ], + ), + ); + } +} diff --git a/lib/model/daily_task_model.dart b/lib/model/daily_task_model.dart index 6290c80..f74391b 100644 --- a/lib/model/daily_task_model.dart +++ b/lib/model/daily_task_model.dart @@ -1,5 +1,6 @@ class TaskModel { - final String assignmentDate; + final DateTime assignmentDate; + final DateTime? reportedDate; final String id; final WorkItem? workItem; final String workItemId; @@ -9,11 +10,9 @@ class TaskModel { final List teamMembers; final List comments; - int get plannedWork => workItem?.plannedWork ?? 0; - int get completedWork => workItem?.completedWork ?? 0; - TaskModel({ required this.assignmentDate, + this.reportedDate, required this.id, required this.workItem, required this.workItemId, @@ -25,15 +24,15 @@ class TaskModel { }); factory TaskModel.fromJson(Map json) { - final workItemJson = json['workItem']; - final workItem = - workItemJson != null ? WorkItem.fromJson(workItemJson) : null; - return TaskModel( - assignmentDate: json['assignmentDate'], - id: json['id'] ?? '', + assignmentDate: DateTime.parse(json['assignmentDate']), + reportedDate: json['reportedDate'] != null + ? DateTime.tryParse(json['reportedDate']) + : null, + id: json['id'], + workItem: + json['workItem'] != null ? WorkItem.fromJson(json['workItem']) : null, workItemId: json['workItemId'], - workItem: workItem, plannedTask: json['plannedTask'], completedTask: json['completedTask'], assignedBy: AssignedBy.fromJson(json['assignedBy']), @@ -47,12 +46,14 @@ class TaskModel { } class WorkItem { + final String? id; final ActivityMaster? activityMaster; final WorkArea? workArea; final int? plannedWork; final int? completedWork; WorkItem({ + this.id, this.activityMaster, this.workArea, this.plannedWork, @@ -61,6 +62,7 @@ class WorkItem { factory WorkItem.fromJson(Map json) { return WorkItem( + id: json['id']?.toString(), activityMaster: json['activityMaster'] != null ? ActivityMaster.fromJson(json['activityMaster']) : null, @@ -78,7 +80,7 @@ class ActivityMaster { ActivityMaster({required this.activityName}); factory ActivityMaster.fromJson(Map json) { - return ActivityMaster(activityName: json['activityName']); + return ActivityMaster(activityName: json['activityName'] ?? ''); } } @@ -90,7 +92,7 @@ class WorkArea { factory WorkArea.fromJson(Map json) { return WorkArea( - areaName: json['areaName'], + areaName: json['areaName'] ?? '', floor: json['floor'] != null ? Floor.fromJson(json['floor']) : null, ); } @@ -104,7 +106,7 @@ class Floor { factory Floor.fromJson(Map json) { return Floor( - floorName: json['floorName'], + floorName: json['floorName'] ?? '', building: json['building'] != null ? Building.fromJson(json['building']) : null, ); @@ -117,34 +119,46 @@ class Building { Building({required this.name}); factory Building.fromJson(Map json) { - return Building(name: json['name']); + return Building(name: json['name'] ?? ''); } } class AssignedBy { + final String id; final String firstName; final String? lastName; - AssignedBy({required this.firstName, this.lastName}); + AssignedBy({ + required this.id, + required this.firstName, + this.lastName, + }); factory AssignedBy.fromJson(Map json) { return AssignedBy( - firstName: json['firstName'], + id: json['id']?.toString() ?? '', + firstName: json['firstName'] ?? '', lastName: json['lastName'], ); } } class TeamMember { + final String id; final String firstName; final String? lastName; - TeamMember({required this.firstName, this.lastName}); + TeamMember({ + required this.id, + required this.firstName, + this.lastName, + }); factory TeamMember.fromJson(Map json) { return TeamMember( - firstName: json['firstName'], - lastName: json['lastName'], + id: json['id']?.toString() ?? '', + firstName: json['firstName']?.toString() ?? '', + lastName: json['lastName']?.toString(), ); } } @@ -152,7 +166,7 @@ class TeamMember { class Comment { final String comment; final TeamMember commentedBy; - final String timestamp; + final DateTime timestamp; Comment({ required this.comment, @@ -162,9 +176,11 @@ class Comment { factory Comment.fromJson(Map json) { return Comment( - comment: json['comment'], - commentedBy: TeamMember.fromJson(json['employee']), - timestamp: json['commentDate'], + comment: json['comment']?.toString() ?? '', + commentedBy: json['employee'] != null + ? TeamMember.fromJson(json['employee']) + : TeamMember(id: '', firstName: '', lastName: null), + timestamp: DateTime.parse(json['commentDate'] ?? ''), ); } } diff --git a/lib/model/employee_model.dart b/lib/model/employee_model.dart index 156ffaf..63669cc 100644 --- a/lib/model/employee_model.dart +++ b/lib/model/employee_model.dart @@ -12,9 +12,11 @@ class EmployeeModel { final String jobRole; final String email; final String phoneNumber; + final String jobRoleID; EmployeeModel({ required this.id, + required this.jobRoleID, required this.employeeId, required this.name, required this.designation, @@ -33,6 +35,7 @@ class EmployeeModel { return EmployeeModel( id: json['id']?.toString() ?? '', employeeId: json['employeeId']?.toString() ?? '', + jobRoleID: json['jobRoleId']?.toString() ?? '', name: '${json['firstName'] ?? ''} ${json['lastName'] ?? ''}'.trim(), designation: json['jobRoleName'] ?? '', checkIn: json['checkInTime'] != null @@ -57,6 +60,7 @@ class EmployeeModel { 'employeeId': employeeId, 'firstName': name.split(' ').first, 'lastName': name.split(' ').length > 1 ? name.split(' ').last : '', + 'jobRoleId': jobRoleID, 'jobRoleName': designation, 'checkInTime': checkIn?.toIso8601String(), 'checkOutTime': checkOut?.toIso8601String(), diff --git a/lib/model/employees/add_employee_bottom_sheet.dart b/lib/model/employees/add_employee_bottom_sheet.dart new file mode 100644 index 0000000..8628539 --- /dev/null +++ b/lib/model/employees/add_employee_bottom_sheet.dart @@ -0,0 +1,245 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/dashboard/add_employee_controller.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/my_button.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_text_style.dart'; +import 'package:marco/controller/dashboard/employees_screen_controller.dart'; +class AddEmployeeBottomSheet extends StatefulWidget { + @override + _AddEmployeeBottomSheetState createState() => _AddEmployeeBottomSheetState(); +} + +class _AddEmployeeBottomSheetState extends State + with UIMixin { + final AddEmployeeController controller = Get.put(AddEmployeeController()); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return GetBuilder( + init: controller, + builder: (_) { + return SingleChildScrollView( + padding: MediaQuery.of(context).viewInsets, + child: Container( + padding: + const EdgeInsets.only(top: 12, left: 24, right: 24, bottom: 24), + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 10, + spreadRadius: 1, + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Drag handle bar + Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MyText.titleMedium( + "Add Employee", + fontWeight: 600, + fontSize: 18, + ), + ], + ), + MySpacing.height(24), + + Form( + key: controller.basicValidator.formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLabel("First Name"), + MySpacing.height(8), + _buildTextField( + hint: "eg: John", + controller: controller.basicValidator + .getController('first_name')!, + validator: controller.basicValidator + .getValidation('first_name'), + keyboardType: TextInputType.name, + ), + MySpacing.height(24), + + _buildLabel("Last Name"), + MySpacing.height(8), + _buildTextField( + hint: "eg: Doe", + controller: controller.basicValidator + .getController('last_name')!, + validator: controller.basicValidator + .getValidation('last_name'), + keyboardType: TextInputType.name, + ), + MySpacing.height(24), + + _buildLabel("Phone Number"), + MySpacing.height(8), + _buildTextField( + hint: "eg: +91 9876543210", + controller: controller.basicValidator + .getController('phone_number')!, + validator: controller.basicValidator + .getValidation('phone_number'), + keyboardType: TextInputType.phone, + ), + MySpacing.height(24), + + _buildLabel("Select Gender"), + MySpacing.height(8), + DropdownButtonFormField( + value: controller.selectedGender, + dropdownColor: contentTheme.background, + isDense: true, + menuMaxHeight: 200, + decoration: _inputDecoration("Select Gender"), + icon: const Icon(Icons.expand_more, size: 20), + items: Gender.values + .map( + (gender) => DropdownMenuItem( + value: gender, + child: MyText.labelMedium( + gender.name.capitalizeFirst!), + ), + ) + .toList(), + onChanged: controller.onGenderSelected, + ), + MySpacing.height(24), + + _buildLabel("Select Role"), + MySpacing.height(8), + DropdownButtonFormField( + value: controller.selectedRoleId, + dropdownColor: contentTheme.background, + isDense: true, + decoration: _inputDecoration("Select Role"), + icon: const Icon(Icons.expand_more, size: 20), + items: controller.roles + .map( + (role) => DropdownMenuItem( + value: role['id'], + child: Text(role['name']), + ), + ) + .toList(), + onChanged: controller.onRoleSelected, + ), + MySpacing.height(24), + + // Buttons row + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyButton.text( + onPressed: () => Navigator.pop(context), + padding: MySpacing.xy(20, 16), + child: MyText.bodySmall("Cancel"), + ), + MySpacing.width(12), + MyButton( + onPressed: () async { + if (controller.basicValidator.validateForm()) { + final success = + await controller.createEmployees(); + if (success) { + // Call refresh logic here via callback or GetX + final employeeController = + Get.find(); + final projectId = + employeeController.selectedProjectId; + if (projectId == null) { + await employeeController + .fetchAllEmployees(); + } else { + await employeeController + .fetchEmployeesByProject(projectId); + } + employeeController + .update(['employee_screen_controller']); + + // Optionally clear form + controller.basicValidator.getController("first_name")?.clear(); +controller.basicValidator.getController("last_name")?.clear(); +controller.basicValidator.getController("phone_number")?.clear(); + + controller.selectedGender = null; + controller.selectedRoleId = null; + controller.update(); + } + } + }, + elevation: 0, + padding: MySpacing.xy(20, 16), + backgroundColor: Colors.blueAccent, + borderRadiusAll: AppStyle.buttonRadius.medium, + child: MyText.bodySmall("Save", + color: contentTheme.onPrimary), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildLabel(String text) => MyText.labelMedium(text); + + Widget _buildTextField({ + required TextEditingController controller, + required String? Function(String?)? validator, + required String hint, + TextInputType keyboardType = TextInputType.text, + }) { + return TextFormField( + controller: controller, + validator: validator, + keyboardType: keyboardType, + decoration: _inputDecoration(hint), + ); + } + + InputDecoration _inputDecoration(String hint) { + return InputDecoration( + hintText: hint, + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(16), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ); + } +} diff --git a/lib/model/employees/employee_detail_bottom_sheet.dart b/lib/model/employees/employee_detail_bottom_sheet.dart new file mode 100644 index 0000000..3990ae4 --- /dev/null +++ b/lib/model/employees/employee_detail_bottom_sheet.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:intl/intl.dart'; +import 'package:marco/controller/dashboard/employees_screen_controller.dart'; + +class EmployeeDetailBottomSheet extends StatefulWidget { + final String employeeId; + + const EmployeeDetailBottomSheet({super.key, required this.employeeId}); + + @override + State createState() => + _EmployeeDetailBottomSheetState(); +} + +class _EmployeeDetailBottomSheetState extends State { + final EmployeesScreenController controller = + Get.put(EmployeesScreenController()); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.fetchEmployeeDetails(widget.employeeId); + }); + } + + @override + void dispose() { + controller.selectedEmployeeDetails.value = null; + super.dispose(); + } + + String _getDisplayValue(dynamic value) { + if (value == null || value.toString().trim().isEmpty || value == 'null') { + return 'NA'; + } + return value.toString(); + } + + String _formatDate(DateTime? date) { + if (date == null || date == DateTime(1)) return 'NA'; + try { + return DateFormat('d/M/yyyy').format(date); + } catch (_) { + return 'NA'; + } + } + + Widget _buildLabelValueRow(IconData icon, String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 20, color: Colors.grey[700]), + MySpacing.width(8), + MyText.bodyMedium('$label:', fontWeight: 600), + MySpacing.width(8), + Expanded(child: MyText.bodyMedium(value, fontWeight: 400)), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + expand: false, + maxChildSize: 0.85, + minChildSize: 0.4, + initialChildSize: 0.6, + builder: (_, controllerScroll) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 10, + offset: Offset(0, -3), + ), + ], + ), + child: Obx(() { + if (controller.isLoadingEmployeeDetails.value) { + return const Center(child: CircularProgressIndicator()); + } + + final employee = controller.selectedEmployeeDetails.value; + + if (employee == null) { + return const Center(child: Text('No employee details found.')); + } + + return SingleChildScrollView( + controller: controllerScroll, + padding: MySpacing.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Drag Handle + Container( + width: 50, + height: 5, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(3), + ), + ), + MySpacing.height(20), + CircleAvatar( + radius: 40, + backgroundColor: Colors.blueGrey[200], + child: Avatar( + firstName: employee.firstName, + lastName: employee.lastName, + size: 60, + ), + ), + MySpacing.height(12), + MyText.titleLarge( + '${employee.firstName} ${employee.lastName}', + fontWeight: 700, + textAlign: TextAlign.center, + ), + if (employee.jobRole.trim().isNotEmpty && + employee.jobRole != 'null') + Padding( + padding: const EdgeInsets.only(top: 6), + child: Chip( + label: Text( + employee.jobRole, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + backgroundColor: Colors.blueAccent, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + ), + ), + MySpacing.height(10), + + // Contact Info Card + _buildInfoCard('Contact Information', [ + _buildLabelValueRow( + Icons.email, 'Email', _getDisplayValue(employee.email)), + _buildLabelValueRow(Icons.phone, 'Phone Number', + _getDisplayValue(employee.phoneNumber)), + ]), + + MySpacing.height(10), + + // Emergency Contact Info Card + _buildInfoCard('Emergency Contact', [ + _buildLabelValueRow(Icons.person, 'Contact Person', + _getDisplayValue(employee.emergencyContactPerson)), + _buildLabelValueRow(Icons.phone_android, 'Contact Number', + _getDisplayValue(employee.emergencyPhoneNumber)), + ]), + + MySpacing.height(10), + + // Personal Info Card + _buildInfoCard('Personal Information', [ + _buildLabelValueRow(Icons.transgender, 'Gender', + _getDisplayValue(employee.gender)), + _buildLabelValueRow(Icons.cake, 'Birth Date', + _formatDate(employee.birthDate)), + _buildLabelValueRow(Icons.work, 'Joining Date', + _formatDate(employee.joiningDate)), + ]), + + MySpacing.height(10), + + // Address Card + _buildInfoCard('Address', [ + _buildLabelValueRow(Icons.home, 'Current Address', + _getDisplayValue(employee.currentAddress)), + _buildLabelValueRow(Icons.home_filled, 'Permanent Address', + _getDisplayValue(employee.permanentAddress)), + ]), + ], + ), + ); + }), + ); + }, + ); + } + + Widget _buildInfoCard(String title, List children) { + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleMedium(title, fontWeight: 700), + MySpacing.height(12), + ...children, + ], + ), + ), + ); + } +} diff --git a/lib/model/employees/employee_details_model.dart b/lib/model/employees/employee_details_model.dart new file mode 100644 index 0000000..cd0ea65 --- /dev/null +++ b/lib/model/employees/employee_details_model.dart @@ -0,0 +1,106 @@ +class EmployeeDetailsModel { + final String id; + final String firstName; + final String lastName; + final String? middleName; + final String? email; + final String gender; + final DateTime? birthDate; + final DateTime? joiningDate; + final String? permanentAddress; + final String? currentAddress; + final String phoneNumber; + final String? emergencyPhoneNumber; + final String? emergencyContactPerson; + final String? aadharNumber; + final bool isActive; + final String? panNumber; + final String? photo; + final String? applicationUserId; + final String jobRoleId; + final bool isSystem; + final String jobRole; + + EmployeeDetailsModel({ + required this.id, + required this.firstName, + required this.lastName, + this.middleName, + this.email, + required this.gender, + this.birthDate, + this.joiningDate, + this.permanentAddress, + this.currentAddress, + required this.phoneNumber, + this.emergencyPhoneNumber, + this.emergencyContactPerson, + this.aadharNumber, + required this.isActive, + this.panNumber, + this.photo, + this.applicationUserId, + required this.jobRoleId, + required this.isSystem, + required this.jobRole, + }); + + factory EmployeeDetailsModel.fromJson(Map json) { + return EmployeeDetailsModel( + id: json['id'], + firstName: json['firstName'], + lastName: json['lastName'], + middleName: json['middleName'], + email: json['email'], + gender: json['gender'], + birthDate: _parseDate(json['birthDate']), + joiningDate: _parseDate(json['joiningDate']), + permanentAddress: json['permanentAddress'], + currentAddress: json['currentAddress'], + phoneNumber: json['phoneNumber'], + emergencyPhoneNumber: json['emergencyPhoneNumber'], + emergencyContactPerson: json['emergencyContactPerson'], + aadharNumber: json['aadharNumber'], + isActive: json['isActive'], + panNumber: json['panNumber'], + photo: json['photo'], + applicationUserId: json['applicationUserId'], + jobRoleId: json['jobRoleId'], + isSystem: json['isSystem'], + jobRole: json['jobRole'], + ); + } + + static DateTime? _parseDate(String? dateStr) { + if (dateStr == null || dateStr == "0001-01-01T00:00:00") { + return null; + } + return DateTime.tryParse(dateStr); + } + + Map toJson() { + return { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'middleName': middleName, + 'email': email, + 'gender': gender, + 'birthDate': birthDate?.toIso8601String(), + 'joiningDate': joiningDate?.toIso8601String(), + 'permanentAddress': permanentAddress, + 'currentAddress': currentAddress, + 'phoneNumber': phoneNumber, + 'emergencyPhoneNumber': emergencyPhoneNumber, + 'emergencyContactPerson': emergencyContactPerson, + 'aadharNumber': aadharNumber, + 'isActive': isActive, + 'panNumber': panNumber, + 'photo': photo, + 'applicationUserId': applicationUserId, + 'jobRoleId': jobRoleId, + 'isSystem': isSystem, + 'jobRole': jobRole, + }; + } +} diff --git a/lib/model/employees/employees_screen_filter_sheet.dart b/lib/model/employees/employees_screen_filter_sheet.dart new file mode 100644 index 0000000..d47957a --- /dev/null +++ b/lib/model/employees/employees_screen_filter_sheet.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/controller/dashboard/employees_screen_controller.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +class EmployeesScreenFilterSheet extends StatelessWidget { + final EmployeesScreenController controller; + final PermissionController permissionController; + + const EmployeesScreenFilterSheet({ + super.key, + required this.controller, + required this.permissionController, + }); + + @override + Widget build(BuildContext context) { + String? tempSelectedProjectId; + bool showProjectList = false; + + final accessibleProjects = controller.projects + .where((project) => + permissionController.isUserAssignedToProject(project.id.toString())) + .toList(); + + return StatefulBuilder(builder: (context, setState) { + List filterWidgets; + + if (showProjectList) { + filterWidgets = []; + + // Add "All Employees" option at the top + filterWidgets.add( + ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + title: MyText.titleSmall('All Employees'), + trailing: + tempSelectedProjectId == null ? const Icon(Icons.check) : null, + onTap: () { + setState(() { + tempSelectedProjectId = null; + showProjectList = false; + }); + }, + ), + ); + + // Add all accessible projects below + if (accessibleProjects.isEmpty) { + filterWidgets.add( + Padding( + padding: const EdgeInsets.all(12.0), + child: Center( + child: MyText.titleSmall( + 'No Projects Assigned', + fontWeight: 600, + ), + ), + ), + ); + } else { + filterWidgets.addAll(accessibleProjects.map((project) { + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + title: MyText.titleSmall(project.name), + trailing: tempSelectedProjectId == project.id.toString() + ? const Icon(Icons.check) + : null, + onTap: () { + setState(() { + tempSelectedProjectId = project.id.toString(); + showProjectList = false; + }); + }, + ); + }).toList()); + } + } else { + String selectedProjectName = 'All Employees'; + if (tempSelectedProjectId != null) { + final selectedProject = accessibleProjects.isNotEmpty + ? accessibleProjects.firstWhere( + (p) => p.id.toString() == tempSelectedProjectId, + orElse: () => accessibleProjects[0], + ) + : null; + + if (selectedProject != null) { + selectedProjectName = selectedProject.name; + } + } + + filterWidgets = [ + Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Align( + alignment: Alignment.centerLeft, + child: MyText.titleSmall( + 'Select Project', + fontWeight: 600, + ), + ), + ), + ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + title: MyText.titleSmall(selectedProjectName), + trailing: const Icon(Icons.arrow_drop_down), + onTap: () => setState(() => showProjectList = true), + ), + ]; + } + + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 8), + child: Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ...filterWidgets, + const Divider(), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 95, 132, 255), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: MyText.titleSmall( + 'Apply Filter', + fontWeight: 600, + color: Colors.white, + ), + onPressed: () { + Navigator.pop(context, { + 'projectId': tempSelectedProjectId, + }); + }, + ), + ), + ), + ], + ), + ), + ); + }); + } +} diff --git a/lib/routes.dart b/lib/routes.dart index c5a5264..dbb0f9a 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -18,6 +18,9 @@ import 'package:marco/view/dashboard/daily_task_screen.dart'; import 'package:marco/view/taskPlaning/report_task_screen.dart'; import 'package:marco/view/taskPlaning/comment_task_screen.dart'; import 'package:marco/view/dashboard/Attendence/attendance_screen.dart'; +import 'package:marco/view/taskPlaning/daily_task_planing.dart'; +import 'package:marco/view/taskPlaning/daily_progress.dart'; +import 'package:marco/view/employees/employees_screen.dart'; class AuthMiddleware extends GetMiddleware { @override @@ -30,9 +33,8 @@ getPageRoute() { var routes = [ GetPage( name: '/', - page: () => AttendanceScreen(), + page: () => AttendanceScreen(), middlewares: [AuthMiddleware()]), - // Dashboard GetPage( name: '/dashboard/attendance', @@ -44,7 +46,7 @@ getPageRoute() { middlewares: [AuthMiddleware()]), GetPage( name: '/dashboard/employees', - page: () => EmployeeScreen(), + page: () => EmployeesScreen(), middlewares: [AuthMiddleware()]), // Employees Creation GetPage( @@ -56,6 +58,14 @@ getPageRoute() { name: '/dashboard/daily-task', page: () => DailyTaskScreen(), middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/daily-task-planing', + page: () => DailyTaskPlaningScreen(), + middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/daily-task-progress', + page: () => DailyProgressReportScreen(), + middlewares: [AuthMiddleware()]), GetPage( name: '/daily-task/report-task', page: () => ReportTaskScreen(), diff --git a/lib/view/dashboard/Attendence/attendance_screen.dart b/lib/view/dashboard/Attendence/attendance_screen.dart index 6e22113..6afaf69 100644 --- a/lib/view/dashboard/Attendence/attendance_screen.dart +++ b/lib/view/dashboard/Attendence/attendance_screen.dart @@ -181,7 +181,6 @@ class _AttendanceScreenState extends State with UIMixin { ), ], ), - MySpacing.height(flexSpacing), Padding( padding: MySpacing.x(flexSpacing / 2), child: MyFlex(children: [ @@ -394,6 +393,7 @@ class _AttendanceScreenState extends State with UIMixin { Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: MyText.titleMedium( @@ -401,6 +401,29 @@ class _AttendanceScreenState extends State with UIMixin { fontWeight: 600, ), ), + Obx(() { + if (attendanceController.isLoading.value) { + return const SizedBox( + height: 20, + width: 20, + child: LinearProgressIndicator(), + ); + } + final dateFormat = DateFormat('dd MMM yyyy'); + final dateRangeText = attendanceController + .startDateAttendance != + null && + attendanceController.endDateAttendance != null + ? '${dateFormat.format(attendanceController.endDateAttendance!)} - ${dateFormat.format(attendanceController.startDateAttendance!)}' + : 'Select date range'; + + return MyText.bodySmall( + dateRangeText, + fontWeight: 600, + color: Colors.grey[700], + overflow: TextOverflow.ellipsis, + ); + }), ], ), ), diff --git a/lib/view/dashboard/daily_task_screen.dart b/lib/view/dashboard/daily_task_screen.dart index 5f6575e..b0bc57e 100644 --- a/lib/view/dashboard/daily_task_screen.dart +++ b/lib/view/dashboard/daily_task_screen.dart @@ -200,7 +200,7 @@ Widget _buildDateRangeButton() { Map> groupedTasks = {}; for (var task in dailyTaskController.dailyTasks) { String dateKey = - DateFormat('dd-MM-yyyy').format(DateTime.parse(task.assignmentDate)); + DateFormat('dd-MM-yyyy').format(task.assignmentDate); groupedTasks.putIfAbsent(dateKey, () => []).add(task); } @@ -369,7 +369,7 @@ Widget _buildDateRangeButton() { 'activity': activityName, 'assigned': assigned, 'taskId': taskId, - 'assignedBy': assignedBy, + 'assignedBy': assignedBy, 'completedWork': completedWork, 'plannedWork': plannedWork, 'assignedOn': assignedOn, diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index cc658e3..7cf2f73 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -1,115 +1,111 @@ import 'package:flutter/material.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:get/get.dart'; -import 'package:marco/helpers/theme/app_theme.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/my_shadow.dart'; -import 'package:marco/helpers/widgets/my_breadcrumb.dart'; -import 'package:marco/helpers/widgets/my_breadcrumb_item.dart'; import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_container.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/view/layouts/layout.dart'; -class DashboardScreen extends StatelessWidget with UIMixin { - DashboardScreen({super.key}); +class DashboardScreen extends StatefulWidget { + const DashboardScreen({super.key}); static const String dashboardRoute = "/dashboard"; static const String employeesRoute = "/dashboard/employees"; static const String projectsRoute = "/dashboard"; static const String attendanceRoute = "/dashboard/attendance"; static const String tasksRoute = "/dashboard/daily-task"; + static const String dailyTasksRoute = "/dashboard/daily-task-planing"; + static const String dailyTasksProgressRoute = + "/dashboard/daily-task-progress"; + @override + State createState() => _DashboardScreenState(); +} + +class _DashboardScreenState extends State with UIMixin { @override Widget build(BuildContext context) { return Layout( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: MySpacing.x(flexSpacing), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText.titleMedium("Dashboard", fontSize: 18, fontWeight: 600), - MyBreadcrumb(children: [MyBreadcrumbItem(name: 'Dashboard')]), - ], - ), - ), - MySpacing.height(flexSpacing), - Padding( - padding: MySpacing.x(flexSpacing / 2), - child: Column( - children: _buildDashboardStats(), - ), - ), - ], + child: SingleChildScrollView( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleMedium("Dashboard", fontWeight: 600), + MySpacing.height(12), + _buildDashboardStats(), + ], + ), ), ); } - List _buildDashboardStats() { + Widget _buildDashboardStats() { final stats = [ - _StatItem( - LucideIcons.gauge, "Dashboard", contentTheme.primary, dashboardRoute), _StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, - attendanceRoute), - _StatItem( - LucideIcons.users, "Employees", contentTheme.warning, employeesRoute), - _StatItem(LucideIcons.logs, "Daily Progress Report", contentTheme.info, - tasksRoute), + DashboardScreen.attendanceRoute), + _StatItem(LucideIcons.users, "Employees", contentTheme.warning, + DashboardScreen.employeesRoute), _StatItem(LucideIcons.logs, "Daily Task Planing", contentTheme.info, - tasksRoute), - _StatItem(LucideIcons.folder, "Projects", contentTheme.secondary, - projectsRoute), + DashboardScreen.dailyTasksRoute), + _StatItem(LucideIcons.list_todo, "Daily Task Progress", contentTheme.info, + DashboardScreen.dailyTasksProgressRoute), ]; - return List.generate( - (stats.length / 2).ceil(), - (index) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildStatCard(stats[index * 2]), - if (index * 2 + 1 < stats.length) - _buildStatCard(stats[index * 2 + 1]), - ], - ), + return LayoutBuilder( + builder: (context, constraints) { + double maxWidth = constraints.maxWidth; + int crossAxisCount = + (maxWidth / 100).floor().clamp(2, 4); + double cardWidth = + (maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount; + + return Wrap( + spacing: 10, + runSpacing: 10, + children: + stats.map((stat) => _buildStatCard(stat, cardWidth)).toList(), + ); + }, ); } - Widget _buildStatCard(_StatItem statItem) { - return Expanded( - child: InkWell( - onTap: () => Get.toNamed(statItem.route), - child: MyCard.bordered( - borderRadiusAll: 10, - border: Border.all(color: Colors.grey.withOpacity(0.2)), - shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), - paddingAll: 24, - height: 140, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildStatCardIcon(statItem), - MySpacing.height(12), - MyText.labelSmall(statItem.title, maxLines: 1), - ], - ), + Widget _buildStatCard(_StatItem statItem, double width) { + return InkWell( + onTap: () => Get.toNamed(statItem.route), + borderRadius: BorderRadius.circular(10), + child: MyCard.bordered( + width: width, + height: 100, + paddingAll: 5, + borderRadiusAll: 10, + border: Border.all(color: Colors.grey.withOpacity(0.15)), + shadow: MyShadow(elevation: 1.5, position: MyShadowPosition.bottom), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildStatCardIcon(statItem), + MySpacing.height(8), + MyText.labelSmall( + statItem.title, + maxLines: 2, + overflow: TextOverflow.visible, + textAlign: TextAlign.center, + ), + ], ), ), ); } Widget _buildStatCardIcon(_StatItem statItem) { - return MyContainer( - paddingAll: 16, - color: statItem.color.withOpacity(0.2), - child: MyContainer( - paddingAll: 8, - color: statItem.color, - child: Icon(statItem.icon, size: 16, color: contentTheme.light), - ), + return MyContainer.rounded( + paddingAll: 10, + color: statItem.color.withOpacity(0.1), + child: Icon(statItem.icon, size: 18, color: statItem.color), ); } } @@ -118,7 +114,7 @@ class _StatItem { final IconData icon; final String title; final Color color; - final String route; // New field to store the route for each stat item + final String route; _StatItem(this.icon, this.title, this.color, this.route); } diff --git a/lib/view/employees/employees_screen.dart b/lib/view/employees/employees_screen.dart new file mode 100644 index 0000000..87d1882 --- /dev/null +++ b/lib/view/employees/employees_screen.dart @@ -0,0 +1,327 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/utils/my_shadow.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb_item.dart'; +import 'package:marco/helpers/widgets/my_card.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/view/layouts/layout.dart'; +import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/model/employees/employees_screen_filter_sheet.dart'; +import 'package:marco/model/employees/add_employee_bottom_sheet.dart'; +import 'package:marco/controller/dashboard/employees_screen_controller.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:marco/model/employees/employee_detail_bottom_sheet.dart'; + +class EmployeesScreen extends StatefulWidget { + const EmployeesScreen({super.key}); + + @override + State createState() => _EmployeesScreenState(); +} + +class _EmployeesScreenState extends State with UIMixin { + final EmployeesScreenController employeeScreenController = + Get.put(EmployeesScreenController()); + final PermissionController permissionController = + Get.put(PermissionController()); + + Future _openFilterSheet() async { + final result = await showModalBottomSheet>( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + builder: (context) => EmployeesScreenFilterSheet( + controller: employeeScreenController, + permissionController: permissionController, + ), + ); + + if (result != null) { + final selectedProjectId = result['projectId'] as String?; + if (selectedProjectId != employeeScreenController.selectedProjectId) { + employeeScreenController.selectedProjectId = selectedProjectId; + + try { + if (selectedProjectId == null) { + await employeeScreenController.fetchAllEmployees(); + } else { + await employeeScreenController + .fetchEmployeesByProject(selectedProjectId); + } + } catch (e) { + debugPrint('Error fetching employees: ${e.toString()}'); + } + + employeeScreenController.update(['employee_screen_controller']); + } + } + } + + Future _refreshEmployees() async { + try { + final projectId = employeeScreenController.selectedProjectId; + if (projectId == null) { + await employeeScreenController.fetchAllEmployees(); + } else { + await employeeScreenController.fetchEmployeesByProject(projectId); + } + } catch (e) { + debugPrint('Error refreshing employee data: ${e.toString()}'); + } + } + + @override + Widget build(BuildContext context) { + return Layout( + floatingActionButton: InkWell( + onTap: () async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + backgroundColor: Colors.transparent, + builder: (context) => AddEmployeeBottomSheet(), + ); + + if (result == true) { + await _refreshEmployees(); + } + }, + borderRadius: BorderRadius.circular(28), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.blueAccent, + borderRadius: BorderRadius.circular(28), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 6, + offset: Offset(0, 3), + ), + ], + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add, color: Colors.white), + SizedBox(width: 8), + Text('Add New Employee', style: TextStyle(color: Colors.white)), + ], + ), + ), + ), + child: GetBuilder( + init: employeeScreenController, + tag: 'employee_screen_controller', + builder: (controller) { + return Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: MySpacing.x(flexSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.titleMedium( + "Employees", + fontSize: 18, + fontWeight: 600, + ), + MyBreadcrumb( + children: [ + MyBreadcrumbItem(name: 'Dashboard'), + MyBreadcrumbItem(name: 'Employees', active: true), + ], + ), + ], + ), + ), + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(flexSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyText.bodyMedium( + "Filter", + fontWeight: 600, + ), + Tooltip( + message: 'Project', + child: InkWell( + borderRadius: BorderRadius.circular(24), + onTap: _openFilterSheet, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: const Padding( + padding: EdgeInsets.all(8), + child: Icon( + Icons.filter_list_alt, + color: Colors.blueAccent, + size: 28, + ), + ), + ), + ), + ), + const SizedBox(width: 8), + MyText.bodyMedium( + "Refresh", + fontWeight: 600, + ), + Tooltip( + message: 'Refresh Data', + child: InkWell( + borderRadius: BorderRadius.circular(24), + onTap: _refreshEmployees, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: const Padding( + padding: EdgeInsets.all(8), + child: Icon( + Icons.refresh, + color: Colors.green, + size: 28, + ), + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: MySpacing.x(flexSpacing), + child: dailyProgressReportTab(), + ), + ], + ), + ], + ); + }, + ), + ); + } + + Widget dailyProgressReportTab() { + return Obx(() { + final isLoading = employeeScreenController.isLoading.value; + final employees = employeeScreenController.employees; + + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (employees.isEmpty) { + return Center( + child: MyText.bodySmall( + "No Assigned Employees Found", + fontWeight: 600, + ), + ); + } + return SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 80), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: employees.map((employee) { + return InkWell( + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => + EmployeeDetailBottomSheet(employeeId: employee.id), + ); + }, + child: MyCard.bordered( + borderRadiusAll: 12, + paddingAll: 10, + margin: MySpacing.bottom(12), + shadow: MyShadow(elevation: 3), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Avatar( + firstName: employee.firstName, + lastName: employee.lastName, + size: 41, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + MyText.titleMedium( + employee.name, + fontWeight: 600, + ), + const SizedBox(width: 6), + MyText.titleSmall( + '(${employee.jobRole})', + fontWeight: 400, + ), + ], + ), + const SizedBox(height: 8), + if (employee.email.isNotEmpty && + employee.email != '-') ...[ + Row( + children: [ + const Icon(Icons.email, + size: 16, color: Colors.red), + const SizedBox(width: 4), + Flexible( + child: MyText.titleSmall( + employee.email, + fontWeight: 400, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 8), + ], + Row( + children: [ + const Icon(Icons.phone, + size: 16, color: Colors.blueAccent), + const SizedBox(width: 4), + MyText.titleSmall( + employee.phoneNumber, + fontWeight: 400, + ), + ], + ), + ], + ), + ), + ], + ) + ], + ), + )); + }).toList(), + ), + ); + }); + } +} diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index e59d55d..7b09b45 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -21,25 +21,26 @@ import 'package:marco/helpers/widgets/avatar.dart'; class Layout extends StatelessWidget { final Widget? child; + final Widget? floatingActionButton; final LayoutController controller = LayoutController(); final topBarTheme = AdminTheme.theme.topBarTheme; final contentTheme = AdminTheme.theme.contentTheme; - final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo(); + final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo(); - Layout({super.key, this.child}); + Layout({super.key, this.child, this.floatingActionButton}); @override Widget build(BuildContext context) { return MyResponsive(builder: (BuildContext context, _, screenMT) { return GetBuilder( - init: controller, - builder: (controller) { - if (screenMT.isMobile || screenMT.isTablet) { - return mobileScreen(); - } else { - return largeScreen(); - } + init: controller, + builder: (controller) { + if (screenMT.isMobile || screenMT.isTablet) { + return mobileScreen(); + } else { + return largeScreen(); + } }); }); } @@ -50,6 +51,22 @@ class Layout extends StatelessWidget { appBar: AppBar( elevation: 0, actions: [ + MySpacing.width(6), + InkWell( + onTap: () { + Get.toNamed('/dashboard'); + }, + borderRadius: BorderRadius.circular(6), + splashColor: contentTheme.primary.withAlpha(20), + child: Padding( + padding: MySpacing.xy(8, 8), + child: Icon( + LucideIcons.layout_dashboard, + size: 18, + color: Colors.blueAccent, + ), + ), + ), MySpacing.width(8), CustomPopupMenu( backdrop: true, @@ -71,9 +88,9 @@ class Layout extends StatelessWidget { backdrop: true, onChange: (_) {}, offsetX: -90, - offsetY: 4, + offsetY: 0, menu: Padding( - padding: MySpacing.xy(8, 8), + padding: MySpacing.xy(0, 8), child: MyContainer.rounded( paddingAll: 0, child: Avatar( @@ -88,6 +105,7 @@ class Layout extends StatelessWidget { ], ), drawer: LeftBar(), + floatingActionButton: floatingActionButton, body: SingleChildScrollView( key: controller.scrollKey, child: child, @@ -99,6 +117,7 @@ class Layout extends StatelessWidget { return Scaffold( key: controller.scaffoldKey, endDrawer: RightBar(), + floatingActionButton: floatingActionButton, body: Row( children: [ LeftBar(isCondensed: ThemeCustomizer.instance.leftBarCondensed), @@ -111,14 +130,16 @@ class Layout extends StatelessWidget { left: 0, bottom: 0, child: SingleChildScrollView( - padding: MySpacing.fromLTRB(0, 58 + flexSpacing, 0, flexSpacing), + padding: + MySpacing.fromLTRB(0, 58 + flexSpacing, 0, flexSpacing), key: controller.scrollKey, child: child, ), ), Positioned(top: 0, left: 0, right: 0, child: TopBar()), ], - )), + ), + ), ], ), ); @@ -128,7 +149,11 @@ class Layout extends StatelessWidget { Widget buildNotification(String title, String description) { return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [MyText.labelLarge(title), MySpacing.height(4), MyText.bodySmall(description)], + children: [ + MyText.labelLarge(title), + MySpacing.height(4), + MyText.bodySmall(description) + ], ); } @@ -142,19 +167,23 @@ class Layout extends StatelessWidget { padding: MySpacing.xy(16, 12), child: MyText.titleMedium("Notification", fontWeight: 600), ), - MyDashedDivider(height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6), + MyDashedDivider( + height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6), Padding( padding: MySpacing.xy(16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildNotification("Welcome to Marco", "Welcome to Marco, we are glad to have you here"), + buildNotification("Welcome to Marco", + "Welcome to Marco, we are glad to have you here"), MySpacing.height(12), - buildNotification("New update available", "There is a new update available for your app"), + buildNotification("New update available", + "There is a new update available for your app"), ], ), ), - MyDashedDivider(height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6), + MyDashedDivider( + height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6), Padding( padding: MySpacing.xy(16, 0), child: Row( @@ -241,6 +270,28 @@ class Layout extends StatelessWidget { ], ), ), + MyButton( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () => {Get.offNamed('/auth/login')}, + borderRadiusAll: AppStyle.buttonRadius.medium, + padding: MySpacing.xy(8, 4), + splashColor: contentTheme.onBackground.withAlpha(20), + backgroundColor: Colors.transparent, + child: Row( + children: [ + Icon( + LucideIcons.log_out, + size: 14, + color: contentTheme.onBackground, + ), + MySpacing.width(8), + MyText.labelMedium( + "Logout", + fontWeight: 600, + ) + ], + ), + ), ], ), ), diff --git a/lib/view/layouts/left_bar.dart b/lib/view/layouts/left_bar.dart index 8f5c0c9..4412066 100644 --- a/lib/view/layouts/left_bar.dart +++ b/lib/view/layouts/left_bar.dart @@ -1,4 +1,3 @@ -// All import statements remain unchanged import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/services/url_service.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; @@ -81,7 +80,7 @@ class _LeftBarState extends State children: [ Center( child: Padding( - padding: MySpacing.y(12), + padding: MySpacing.fromLTRB(0, 24, 0, 0), child: InkWell( onTap: () => Get.toNamed('/home'), child: Image.asset( @@ -115,7 +114,7 @@ class _LeftBarState extends State isCondensed: isCondensed, route: '/dashboard'), NavigationItem( - iconData: LucideIcons.layout_template, + iconData: LucideIcons.scan_face, title: "Attendance", isCondensed: isCondensed, route: '/dashboard/attendance'), @@ -125,15 +124,15 @@ class _LeftBarState extends State isCondensed: isCondensed, route: '/dashboard/employees'), NavigationItem( - iconData: LucideIcons.list, - title: "Daily Progress Report", - isCondensed: isCondensed, - route: '/dashboard/daily-task'), - NavigationItem( - iconData: LucideIcons.list_todo, + iconData: LucideIcons.logs, title: "Daily Task Planing", isCondensed: isCondensed, route: '/dashboard/daily-task-planing'), + NavigationItem( + iconData: LucideIcons.list_todo, + title: "Daily Progress Report", + isCondensed: isCondensed, + route: '/dashboard/daily-task-progress'), ], ), ), @@ -148,9 +147,7 @@ class _LeftBarState extends State Widget userInfoSection() { if (employeeInfo == null) { - return Center( - child: - CircularProgressIndicator()); // Show loading indicator if employeeInfo is not yet loaded. + return Center(child: CircularProgressIndicator()); } return Padding( @@ -245,9 +242,7 @@ class _MenuWidgetState extends State } void onChangeMenuActive(String key) { - if (key != widget.title) { - // onChangeExpansion(false); - } + if (key != widget.title) {} } void onChangeExpansion(value) { @@ -271,22 +266,16 @@ class _MenuWidgetState extends State if (hideFn != null) { hideFn!(); } - // popupShowing = false; } @override Widget build(BuildContext context) { - // var route = Uri.base.fragment; - // isActive = widget.children.any((element) => element.route == route); - if (widget.isCondensed) { return CustomPopupMenu( backdrop: true, show: popupShowing, hideFn: (hide) => hideFn = hide, - onChange: (_) { - // popupShowing = _; - }, + onChange: (_) {}, placement: CustomPopupMenuPlacement.right, menu: MouseRegion( cursor: SystemMouseCursors.click, @@ -407,7 +396,6 @@ class _MenuWidgetState extends State void dispose() { _controller.dispose(); super.dispose(); - // LeftbarObserver.detachListener(widget.title); } } diff --git a/lib/view/taskPlaning/daily_progress.dart b/lib/view/taskPlaning/daily_progress.dart new file mode 100644 index 0000000..4bb7692 --- /dev/null +++ b/lib/view/taskPlaning/daily_progress.dart @@ -0,0 +1,602 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/utils/my_shadow.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb_item.dart'; +import 'package:marco/helpers/widgets/my_card.dart'; +import 'package:marco/helpers/widgets/my_container.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/view/layouts/layout.dart'; +import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/controller/dashboard/daily_task_controller.dart'; +import 'package:marco/model/dailyTaskPlaning/daily_progress_report_filter.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:marco/model/dailyTaskPlaning/comment_task_bottom_sheet.dart'; +import 'package:marco/model/dailyTaskPlaning/report_task_bottom_sheet.dart'; + +class DailyProgressReportScreen extends StatefulWidget { + const DailyProgressReportScreen({super.key}); + + @override + State createState() => + _DailyProgressReportScreenState(); +} + +class TaskChartData { + final String label; + final num value; + final Color color; + + TaskChartData(this.label, this.value, this.color); +} + +class _DailyProgressReportScreenState extends State + with UIMixin { + final DailyTaskController dailyTaskController = + Get.put(DailyTaskController()); + final PermissionController permissionController = + Get.put(PermissionController()); + + @override + Widget build(BuildContext context) { + return Layout( + child: GetBuilder( + init: dailyTaskController, + tag: 'daily_progress_report_controller', + builder: (controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(), + MySpacing.height(flexSpacing), + _buildActionBar(), + Padding( + padding: MySpacing.x(flexSpacing), + child: _buildDailyProgressReportTab(), + ), + ], + ); + }, + ), + ); + } + + Widget _buildHeader() { + return Padding( + padding: MySpacing.x(flexSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.titleMedium("Daily Progress Report", + fontSize: 18, fontWeight: 600), + MyBreadcrumb( + children: [ + MyBreadcrumbItem(name: 'Dashboard'), + MyBreadcrumbItem(name: 'Daily Progress Report', active: true), + ], + ), + ], + ), + ); + } + + Widget _buildActionBar() { + return Padding( + padding: MySpacing.x(flexSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _buildActionItem( + label: "Filter", + icon: Icons.filter_list_alt, + tooltip: 'Filter Project', + color: Colors.blueAccent, + onTap: _openFilterSheet, + ), + const SizedBox(width: 8), + _buildActionItem( + label: "Refresh", + icon: Icons.refresh, + tooltip: 'Refresh Data', + color: Colors.green, + onTap: _refreshData, + ), + ], + ), + ); + } + + Widget _buildActionItem({ + required String label, + required IconData icon, + required String tooltip, + required VoidCallback onTap, + required Color color, + }) { + return Row( + children: [ + MyText.bodyMedium(label, fontWeight: 600), + Tooltip( + message: tooltip, + child: InkWell( + borderRadius: BorderRadius.circular(24), + onTap: onTap, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon(icon, color: color, size: 28), + ), + ), + ), + ), + ], + ); + } + + Future _openFilterSheet() async { + final result = await showModalBottomSheet>( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + builder: (context) => DailyProgressReportFilter( + controller: dailyTaskController, + permissionController: permissionController, + ), + ); + + if (result != null) { + final selectedProjectId = result['projectId'] as String?; + if (selectedProjectId != null && + selectedProjectId != dailyTaskController.selectedProjectId) { + dailyTaskController.selectedProjectId = selectedProjectId; + try { + await dailyTaskController.fetchProjects(); + } catch (e) { + debugPrint('Error fetching projects: $e'); + } + dailyTaskController.update(['daily_progress_report_controller']); + } + } + } + + Future _refreshData() async { + final projectId = dailyTaskController.selectedProjectId; + if (projectId != null) { + try { + await dailyTaskController.fetchTaskData(projectId); + } catch (e) { + debugPrint('Error refreshing task data: $e'); + } + } + } + + void _showTeamMembersBottomSheet(List members) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + isDismissible: true, + enableDrag: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + builder: (context) { + return GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(12)), + ), + padding: const EdgeInsets.fromLTRB(16, 24, 16, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleMedium( + 'Team Members', + fontWeight: 600, + ), + const SizedBox(height: 8), + const Divider(thickness: 1), + const SizedBox(height: 8), + ...members.map((member) { + final firstName = member.firstName ?? 'Unnamed'; + final lastName = member.lastName ?? 'User'; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: Avatar( + firstName: firstName, + lastName: lastName, + size: 31, + ), + title: MyText.bodyMedium( + '$firstName $lastName', + fontWeight: 600, + ), + ); + }), + const SizedBox(height: 8), + ], + ), + ), + ); + }, + ); + } + + Widget _buildDailyProgressReportTab() { + return Obx(() { + final isLoading = dailyTaskController.isLoading.value; + final groupedTasks = dailyTaskController.groupedDailyTasks; + + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (groupedTasks.isEmpty) { + return Center( + child: MyText.bodySmall( + "No Progress Report Found", + fontWeight: 600, + ), + ); + } + + final sortedDates = groupedTasks.keys.toList() + ..sort((a, b) => b.compareTo(a)); + + return MyCard.bordered( + borderRadiusAll: 4, + border: Border.all(color: Colors.grey.withOpacity(0.2)), + shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), + paddingAll: 8, + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: sortedDates.length, + separatorBuilder: (_, __) => Column( + children: [ + const SizedBox(height: 12), + Divider(color: Colors.grey.withOpacity(0.3), thickness: 1), + const SizedBox(height: 12), + ], + ), + itemBuilder: (context, dateIndex) { + final dateKey = sortedDates[dateIndex]; + final tasksForDate = groupedTasks[dateKey]!; + final date = DateTime.tryParse(dateKey); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () => dailyTaskController.toggleDate(dateKey), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.bodyMedium( + date != null + ? DateFormat('dd MMM yyyy').format(date) + : dateKey, + fontWeight: 700, + ), + Obx(() => Icon( + dailyTaskController.expandedDates.contains(dateKey) + ? Icons.remove_circle + : Icons.add_circle, + color: Colors.blueAccent, + )), + ], + ), + ), + Obx(() { + if (!dailyTaskController.expandedDates.contains(dateKey)) { + return const SizedBox.shrink(); + } + + return Column( + children: tasksForDate.asMap().entries.map((entry) { + final task = entry.value; + final index = entry.key; + + final activityName = + task.workItem?.activityMaster?.activityName ?? 'N/A'; + final location = [ + task.workItem?.workArea?.floor?.building?.name, + task.workItem?.workArea?.floor?.floorName, + task.workItem?.workArea?.areaName + ].where((e) => e?.isNotEmpty ?? false).join(' > '); + + final planned = task.plannedTask; + final completed = task.completedTask; + final progress = (planned != 0) + ? (completed / planned).clamp(0.0, 1.0) + : 0.0; + + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: MyContainer( + paddingAll: 12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium(activityName, + fontWeight: 600), + const SizedBox(height: 2), + MyText.bodySmall(location, + color: Colors.grey), + const SizedBox(height: 8), + GestureDetector( + onTap: () => _showTeamMembersBottomSheet( + task.teamMembers), + child: Row( + children: [ + const Icon(Icons.group, + size: 18, color: Colors.blueAccent), + const SizedBox(width: 6), + MyText.bodyMedium('Team', + color: Colors.blueAccent, + fontWeight: 600), + ], + ), + ), + const SizedBox(height: 8), + MyText.bodySmall( + "Completed: $completed / $planned", + fontWeight: 600, + color: Colors.black87, + ), + const SizedBox(height: 6), + Stack( + children: [ + Container( + height: 5, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: + BorderRadius.circular(6), + ), + ), + FractionallySizedBox( + widthFactor: progress, + child: Container( + height: 5, + decoration: BoxDecoration( + color: progress >= 1.0 + ? Colors.green + : progress >= 0.5 + ? Colors.amber + : Colors.red, + borderRadius: + BorderRadius.circular(6), + ), + ), + ), + ], + ), + const SizedBox(height: 4), + MyText.bodySmall( + "${(progress * 100).toStringAsFixed(1)}%", + fontWeight: 500, + color: progress >= 1.0 + ? Colors.green[700] + : progress >= 0.5 + ? Colors.amber[800] + : Colors.red[700], + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (task.reportedDate == null || + task.reportedDate.toString().isEmpty) + OutlinedButton.icon( + icon: const Icon(Icons.report, + size: 18, + color: Colors.blueAccent), + label: const Text('Report', + style: TextStyle( + color: Colors.blueAccent)), + style: OutlinedButton.styleFrom( + side: const BorderSide( + color: Colors.blueAccent), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + textStyle: + const TextStyle(fontSize: 14), + ), + onPressed: () { + final activityName = task + .workItem + ?.activityMaster + ?.activityName ?? + 'N/A'; + final assigned = + '${(task.plannedTask - completed)}'; + final assignedBy = + "${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}"; + final assignedOn = + DateFormat('dd-MM-yyyy').format( + task.assignmentDate); + final taskId = task.id; + final location = [ + task.workItem?.workArea?.floor + ?.building?.name, + task.workItem?.workArea?.floor + ?.floorName, + task.workItem?.workArea?.areaName, + ] + .where((e) => + e != null && e.isNotEmpty) + .join(' > '); + final teamMembers = task.teamMembers + .map((e) => e.firstName) + .toList(); + + final taskData = { + 'activity': activityName, + 'assigned': assigned, + 'taskId': taskId, + 'assignedBy': assignedBy, + 'completed': completed, + 'assignedOn': assignedOn, + 'location': location, + 'teamSize': + task.teamMembers.length, + 'teamMembers': teamMembers, + }; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: + const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical( + top: Radius.circular( + 16)), + ), + builder: (_) => Padding( + padding: MediaQuery.of(context) + .viewInsets, + child: ReportTaskBottomSheet( + taskData: taskData), + ), + ); + }, + ), + const SizedBox(width: 8), + OutlinedButton.icon( + icon: const Icon(Icons.comment, + size: 18, color: Colors.blueAccent), + label: const Text('Comment', + style: TextStyle( + color: Colors.blueAccent)), + style: OutlinedButton.styleFrom( + side: const BorderSide( + color: Colors.blueAccent), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + textStyle: + const TextStyle(fontSize: 14), + ), + onPressed: () { + final activityName = task + .workItem + ?.activityMaster + ?.activityName ?? + 'N/A'; + final plannedTask = task.plannedTask; + final completed = task.completedTask; + final assigned = + '${(plannedTask - completed)}'; + final plannedWork = + plannedTask.toString(); + final completedWork = + completed.toString(); + final assignedBy = + "${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}"; + final assignedOn = + DateFormat('yyyy-MM-dd') + .format(task.assignmentDate); + final taskId = task.id; + final location = [ + task.workItem?.workArea?.floor + ?.building?.name, + task.workItem?.workArea?.floor + ?.floorName, + task.workItem?.workArea?.areaName, + ] + .where((e) => + e != null && e.isNotEmpty) + .join(' > '); + + final teamMembers = task.teamMembers + .map((e) => + '${e.firstName} ${e.lastName}') + .toList(); + + final taskComments = + task.comments.map((comment) { + final isoDate = comment.timestamp + .toIso8601String(); + final commenterName = comment + .commentedBy + .firstName + .isNotEmpty + ? "${comment.commentedBy.firstName} ${comment.commentedBy.lastName ?? ''}" + .trim() + : "Unknown"; + + return { + 'text': comment.comment, + 'date': isoDate, + 'commentedBy': commenterName, + }; + }).toList(); + + final taskData = { + 'activity': activityName, + 'assigned': assigned, + 'taskId': taskId, + 'assignedBy': assignedBy, + 'completedWork': completedWork, + 'plannedWork': plannedWork, + 'assignedOn': assignedOn, + 'location': location, + 'teamSize': task.teamMembers.length, + 'teamMembers': teamMembers, + 'taskComments': taskComments, + }; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => + CommentTaskBottomSheet( + taskData: taskData, + onCommentSuccess: () { + _refreshData(); + Navigator.of(context).pop(); + }, + ), + ); + }, + ), + ], + ) + ], + ), + ), + ), + if (index != tasksForDate.length - 1) + Divider( + color: Colors.grey.withOpacity(0.2), + thickness: 1, + height: 1), + ], + ); + }).toList(), + ); + }) + ], + ); + }, + ), + ); + }); + } +} diff --git a/lib/view/taskPlaning/daily_task_planing.dart b/lib/view/taskPlaning/daily_task_planing.dart new file mode 100644 index 0000000..d9afd23 --- /dev/null +++ b/lib/view/taskPlaning/daily_task_planing.dart @@ -0,0 +1,485 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/utils/my_shadow.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb_item.dart'; +import 'package:marco/helpers/widgets/my_card.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/view/layouts/layout.dart'; +import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/model/dailyTaskPlaning/daily_task_planing_filter.dart'; +import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; +import 'package:marco/model/dailyTaskPlaning/assign_task_bottom_sheet .dart'; + +class DailyTaskPlaningScreen extends StatefulWidget { + DailyTaskPlaningScreen({super.key}); + + @override + State createState() => _DailyTaskPlaningScreenState(); +} + +class _DailyTaskPlaningScreenState extends State + with UIMixin { + final DailyTaskPlaningController dailyTaskPlaningController = + Get.put(DailyTaskPlaningController()); + final PermissionController permissionController = + Get.put(PermissionController()); + + @override + Widget build(BuildContext context) { + return Layout( + child: GetBuilder( + init: dailyTaskPlaningController, + tag: 'daily_task_planing_controller', + builder: (controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: MySpacing.x(flexSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.titleMedium("Daily Task Planning", + fontSize: 18, fontWeight: 600), + MyBreadcrumb( + children: [ + MyBreadcrumbItem(name: 'Dashboard'), + MyBreadcrumbItem( + name: 'Daily Task Planning', active: true), + ], + ), + ], + ), + ), + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(flexSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyText.bodyMedium( + "Filter", + fontWeight: 600, + ), + Tooltip( + message: 'Project', + child: InkWell( + borderRadius: BorderRadius.circular(24), + onTap: () async { + final result = + await showModalBottomSheet>( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12)), + ), + builder: (context) => DailyTaskPlaningFilter( + controller: dailyTaskPlaningController, + permissionController: permissionController, + ), + ); + + if (result != null) { + final selectedProjectId = + result['projectId'] as String?; + + if (selectedProjectId != null && + selectedProjectId != + dailyTaskPlaningController + .selectedProjectId) { + // Update the controller's selected project ID + dailyTaskPlaningController.selectedProjectId = + selectedProjectId; + + try { + // Fetch tasks for the new project + await dailyTaskPlaningController + .fetchTaskData(selectedProjectId); + } catch (e) { + debugPrint( + 'Error fetching task data: ${e.toString()}'); + } + + // Update the UI + dailyTaskPlaningController + .update(['daily_task_planing_controller']); + } + } + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.filter_list_alt, + color: Colors.blueAccent, + size: 28, + ), + ), + ), + ), + ), + const SizedBox(width: 8), + MyText.bodyMedium( + "Refresh", + fontWeight: 600, + ), + Tooltip( + message: 'Refresh Data', + child: InkWell( + borderRadius: BorderRadius.circular(24), + onTap: () async { + final projectId = + dailyTaskPlaningController.selectedProjectId; + if (projectId != null) { + try { + await dailyTaskPlaningController + .fetchTaskData(projectId); + } catch (e) { + debugPrint( + 'Error refreshing task data: ${e.toString()}'); + } + } + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.refresh, + color: Colors.green, + size: 28, + ), + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: MySpacing.x(flexSpacing), + child: dailyProgressReportTab(), + ), + ], + ); + }, + ), + ); + } + + Widget dailyProgressReportTab() { + return Obx(() { + final isLoading = dailyTaskPlaningController.isLoading.value; + final dailyTasks = dailyTaskPlaningController.dailyTasks; + + if (isLoading) { + return Center(child: CircularProgressIndicator()); + } + + if (dailyTasks.isEmpty) { + return Center( + child: MyText.bodySmall( + "No Progress Report Found", + fontWeight: 600, + ), + ); + } + + final buildingExpansionState = {}; + final floorExpansionState = {}; + + Widget buildExpandIcon(bool isExpanded) { + return Container( + width: 32, + height: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade200, + ), + child: Icon( + isExpanded ? Icons.remove : Icons.add, + size: 20, + color: Colors.black87, + ), + ); + } + + return StatefulBuilder(builder: (context, setMainState) { + final filteredBuildings = dailyTasks.expand((task) { + return task.buildings.where((building) { + return building.floors.any((floor) => + floor.workAreas.any((area) => area.workItems.isNotEmpty)); + }); + }).toList(); + + if (filteredBuildings.isEmpty) { + return Center( + child: MyText.bodySmall( + "No Progress Report Found", + fontWeight: 600, + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: filteredBuildings.map((building) { + final buildingKey = building.id.toString(); + + return MyCard.bordered( + borderRadiusAll: 12, + paddingAll: 0, + margin: MySpacing.bottom(12), + shadow: MyShadow(elevation: 3), + child: Theme( + data: Theme.of(context) + .copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + onExpansionChanged: (expanded) { + setMainState(() { + buildingExpansionState[buildingKey] = expanded; + }); + }, + trailing: buildExpandIcon( + buildingExpansionState[buildingKey] ?? false), + tilePadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 0), + collapsedShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + leading: Container( + decoration: BoxDecoration( + color: Colors.blueAccent.withOpacity(0.1), + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all(8), + child: Icon( + Icons.location_city_rounded, + color: Colors.blueAccent, + size: 24, + ), + ), + title: MyText.titleMedium( + building.name, + fontWeight: 700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + childrenPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 0), + children: building.floors.expand((floor) { + final validWorkAreas = floor.workAreas + .where((area) => area.workItems.isNotEmpty); + + // For each valid work area, return a Floor+WorkArea ExpansionTile + return validWorkAreas.map((area) { + final floorWorkAreaKey = + "${buildingKey}_${floor.floorName}_${area.areaName}"; + final isExpanded = + floorExpansionState[floorWorkAreaKey] ?? false; + + return ExpansionTile( + onExpansionChanged: (expanded) { + setMainState(() { + floorExpansionState[floorWorkAreaKey] = expanded; + }); + }, + trailing: Icon( + isExpanded + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + size: 28, + color: Colors.black54, + ), + tilePadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 0), + title: Wrap( + spacing: 16, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + MyText.titleSmall( + "Floor: ${floor.floorName}", + fontWeight: 600, + color: Colors.teal, + maxLines: null, + overflow: TextOverflow.visible, + softWrap: true, + ), + MyText.titleSmall( + "Work Area: ${area.areaName}", + fontWeight: 600, + color: Colors.blueGrey, + maxLines: null, + overflow: TextOverflow.visible, + softWrap: true, + ), + ], + ), + childrenPadding: const EdgeInsets.only( + left: 16, right: 0, bottom: 8), + children: area.workItems.map((wItem) { + final item = wItem.workItem; + final completed = item.completedWork ?? 0; + final planned = item.plannedWork ?? 0; + final progress = (planned == 0) + ? 0.0 + : (completed / planned).clamp(0.0, 1.0); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: MyText.bodyMedium( + item.activityMaster?.name ?? + "No Activity", + fontWeight: 600, + maxLines: 2, + overflow: TextOverflow.visible, + softWrap: true, + ), + ), + MySpacing.width(8), + if (item.workCategoryMaster?.name != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: + BorderRadius.circular(20), + ), + child: MyText.bodySmall( + item.workCategoryMaster!.name!, + fontWeight: 500, + color: Colors.blue.shade800, + ), + ), + ], + ), + MySpacing.height(4), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + MySpacing.height(8), + MyText.bodySmall( + "Completed: $completed / $planned", + fontWeight: 600, + color: const Color.fromARGB( + 221, 0, 0, 0), + ), + ], + ), + ), + MySpacing.width(16), + if (progress < 1.0) + IconButton( + icon: Icon( + Icons.person_add_alt_1_rounded, + color: const Color.fromARGB( + 255, 46, 161, 233), + ), + onPressed: () { + final pendingTask = + (planned - completed) + .clamp(0, planned); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical( + top: Radius.circular(16)), + ), + builder: (context) => + AssignTaskBottomSheet( + buildingName: building.name, + floorName: floor.floorName, + workAreaName: area.areaName, + workLocation: area.areaName, + activityName: + item.activityMaster?.name ?? + "Unknown Activity", + pendingTask: pendingTask, + workItemId: item.id.toString(), + assignmentDate: DateTime.now(), + ), + ); + }, + ), + ], + ), + MySpacing.height(8), + Stack( + children: [ + Container( + height: 5, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(6), + ), + ), + FractionallySizedBox( + widthFactor: progress, + child: Container( + height: 5, + decoration: BoxDecoration( + color: progress >= 1.0 + ? Colors.green + : (progress >= 0.5 + ? Colors.amber + : Colors.red), + borderRadius: + BorderRadius.circular(6), + ), + ), + ), + ], + ), + SizedBox(height: 4), + MyText.bodySmall( + "${(progress * 100).toStringAsFixed(1)}%", + fontWeight: 500, + color: progress >= 1.0 + ? Colors.green[700] + : (progress >= 0.5 + ? Colors.amber[800] + : Colors.red[700]), + ), + ], + ), + ); + }).toList(), + ); + }).toList(); + }).toList(), + ), + ), + ); + }).toList(), + ); + }); + }); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 0386f63..b2e3adf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,9 @@ dependencies: image: ^4.0.17 image_picker: ^1.0.7 logger: ^2.0.2 + flutter_image_compress: ^2.1.0 + path_provider: ^2.1.2 + path: ^1.9.0 dev_dependencies: flutter_test: sdk: flutter @@ -89,17 +92,17 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/ - - assets/lang/ - assets/avatar/ - - assets/logo/ + - assets/coin/ + - assets/country/ - assets/data/ - assets/dummy/ - - assets/social/ - - assets/country/ - - assets/coin/ - assets/dummy/ecommerce/ - assets/dummy/single_product/ + - assets/lang/ + - assets/logo/ - assets/logo/loading_logo.png + - assets/social/ # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg