From 3a449441faa7afacbd3d076395556907f7c1b996 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 12 Jun 2025 17:28:06 +0530 Subject: [PATCH] feat: Enhance project and task management features - Added clearProjects method in ProjectController to reset project states. - Updated fetchProjects and updateSelectedProject methods for better state management. - Enhanced ReportTaskController to support image uploads with base64 encoding. - Modified ApiService to handle image data in report and comment tasks. - Integrated ProjectController in AuthService to fetch projects upon login. - Updated LocalStorage to clear selectedProjectId on logout. - Introduced ImageViewerDialog for displaying images in a dialog. - Enhanced CommentTaskBottomSheet and ReportTaskBottomSheet to support image attachments. - Improved AttendanceScreen to handle project selection and data fetching more robustly. - Refactored EmployeesScreen to manage employee data based on project selection. - Updated Layout to handle project selection and display appropriate messages. - Enhanced DailyProgressReportScreen and DailyTaskPlaningScreen to reactively fetch task data based on project changes. - Added photo_view dependency for improved image handling. --- .../employees_screen_controller.dart | 38 +- lib/controller/project_controller.dart | 17 +- .../task_planing/report_task_controller.dart | 29 +- lib/helpers/services/api_service.dart | 4 + lib/helpers/services/auth_service.dart | 4 +- .../services/storage/local_storage.dart | 33 +- lib/helpers/widgets/image_viewer_dialog.dart | 122 +++++ .../comment_task_bottom_sheet.dart | 447 +++++++++++++++--- .../report_task_bottom_sheet.dart | 150 +++++- lib/view/auth/mpin_auth_screen.dart | 13 +- .../Attendence/attendance_screen.dart | 59 ++- lib/view/employees/employees_screen.dart | 164 ++++--- lib/view/layouts/layout.dart | 90 ++-- lib/view/taskPlaning/daily_progress.dart | 29 +- lib/view/taskPlaning/daily_task_planing.dart | 18 +- lib/view/taskPlaning/report_task_screen.dart | 2 +- pubspec.yaml | 1 + 17 files changed, 982 insertions(+), 238 deletions(-) create mode 100644 lib/helpers/widgets/image_viewer_dialog.dart diff --git a/lib/controller/dashboard/employees_screen_controller.dart b/lib/controller/dashboard/employees_screen_controller.dart index bdefcef..642586c 100644 --- a/lib/controller/dashboard/employees_screen_controller.dart +++ b/lib/controller/dashboard/employees_screen_controller.dart @@ -5,6 +5,7 @@ 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'; +import 'package:marco/controller/project_controller.dart'; final Logger log = Logger(); @@ -12,8 +13,9 @@ class EmployeesScreenController extends GetxController { List attendances = []; List projects = []; String? selectedProjectId; - List employees = []; List employeeDetails = []; + RxBool isAllEmployeeSelected = false.obs; + RxList employees = [].obs; RxBool isLoading = false.obs; RxMap uploadingStates = {}.obs; @@ -24,7 +26,17 @@ class EmployeesScreenController extends GetxController { void onInit() { super.onInit(); fetchAllProjects(); - fetchAllEmployees(); + + final projectId = Get.find().selectedProject?.id; + + if (projectId != null) { + selectedProjectId = projectId; + fetchEmployeesByProject(projectId); + } else if (isAllEmployeeSelected.value) { + fetchAllEmployees(); + } else { + clearEmployees(); + } } Future fetchAllProjects() async { @@ -41,18 +53,27 @@ class EmployeesScreenController extends GetxController { update(); } + void clearEmployees() { + employees.clear(); // Correct way to clear RxList + log.i("Employees cleared"); + update(['employee_screen_controller']); + } + Future fetchAllEmployees() async { isLoading.value = true; await _handleApiCall( ApiService.getAllEmployees, onSuccess: (data) { - employees = data.map((json) => EmployeeModel.fromJson(json)).toList(); + employees.assignAll(data.map((json) => EmployeeModel.fromJson(json))); log.i("All Employees fetched: ${employees.length} employees loaded."); }, - onEmpty: () => log.w("No Employee data found or API call failed."), + onEmpty: () { + employees.clear(); // Always clear on empty + log.w("No Employee data found or API call failed."); + }, ); isLoading.value = false; - update(); + update(['employee_screen_controller']); } Future fetchEmployeesByProject(String? projectId) async { @@ -65,22 +86,21 @@ class EmployeesScreenController extends GetxController { await _handleApiCall( () => ApiService.getAllEmployeesByProject(projectId), onSuccess: (data) { - employees = data.map((json) => EmployeeModel.fromJson(json)).toList(); + employees.assignAll(data.map((json) => EmployeeModel.fromJson(json))); for (var emp in employees) { uploadingStates[emp.id] = false.obs; } log.i("Employees fetched: ${employees.length} for project $projectId"); - update(); }, onEmpty: () { + employees.clear(); log.w("No employees found for project $projectId."); - employees = []; - update(); }, onError: (e) => log.e("Error fetching employees for project $projectId: $e"), ); isLoading.value = false; + update(['employee_screen_controller']); } Future _handleApiCall( diff --git a/lib/controller/project_controller.dart b/lib/controller/project_controller.dart index bd5ba48..6594d78 100644 --- a/lib/controller/project_controller.dart +++ b/lib/controller/project_controller.dart @@ -28,6 +28,19 @@ class ProjectController extends GetxController { fetchProjects(); } + void clearProjects() { + projects.clear(); + selectedProjectId = null; + isProjectSelectionExpanded.value = false; + isProjectListExpanded.value = false; + isProjectDropdownExpanded.value = false; + isLoadingProjects.value = false; + isLoading.value = false; + uploadingStates.clear(); + LocalStorage.saveString('selectedProjectId', ''); + update(); + } + /// Fetches projects and initializes selected project. Future fetchProjects() async { isLoadingProjects.value = true; @@ -62,6 +75,8 @@ class ProjectController extends GetxController { Future updateSelectedProject(String projectId) async { selectedProjectId?.value = projectId; await LocalStorage.saveString('selectedProjectId', projectId); - update(); + update([ + 'selected_project' + ]); } } diff --git a/lib/controller/task_planing/report_task_controller.dart b/lib/controller/task_planing/report_task_controller.dart index c921caa..51bc584 100644 --- a/lib/controller/task_planing/report_task_controller.dart +++ b/lib/controller/task_planing/report_task_controller.dart @@ -9,6 +9,7 @@ import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; +import 'dart:convert'; final Logger logger = Logger(); @@ -96,6 +97,7 @@ class ReportTaskController extends MyController { required int completedTask, required List> checklist, required DateTime reportedDate, + List? images, }) async { logger.i("Starting task report..."); @@ -132,12 +134,24 @@ class ReportTaskController extends MyController { try { isLoading.value = true; + List>? imageData; + if (images != null && images.isNotEmpty) { + imageData = await Future.wait(images.map((file) async { + final bytes = await file.readAsBytes(); + final base64Image = base64Encode(bytes); + return { + "fileName": file.path.split('/').last, + "fileData": base64Image, + }; + })); + } final success = await ApiService.reportTask( id: projectId, comment: commentField, - completedTask: completedTask, + completedTask: completedWorkInt, checkList: checklist, + images: imageData, ); if (success) { @@ -169,6 +183,7 @@ class ReportTaskController extends MyController { Future commentTask({ required String projectId, required String comment, + List? images, }) async { logger.i("Starting task comment..."); @@ -184,10 +199,22 @@ class ReportTaskController extends MyController { try { isLoading.value = true; + List>? imageData; + if (images != null && images.isNotEmpty) { + imageData = await Future.wait(images.map((file) async { + final bytes = await file.readAsBytes(); + final base64Image = base64Encode(bytes); + return { + "fileName": file.path.split('/').last, + "fileData": base64Image, + }; + })); + } final success = await ApiService.commentTask( id: projectId, comment: commentField, + images: imageData, ); if (success) { diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 0508b81..1b5a4f5 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -343,6 +343,7 @@ class ApiService { required int completedTask, required String comment, required List> checkList, + List>? images, }) async { final body = { "id": id, @@ -350,6 +351,7 @@ class ApiService { "comment": comment, "reportedDate": DateTime.now().toUtc().toIso8601String(), "checkList": checkList, + if (images != null && images.isNotEmpty) "images": images, }; final response = await _postRequest(ApiEndpoints.reportTask, body); @@ -373,11 +375,13 @@ class ApiService { static Future commentTask({ required String id, required String comment, + List>? images, }) async { final body = { "taskAllocationId": id, "comment": comment, "commentDate": DateTime.now().toUtc().toIso8601String(), + if (images != null && images.isNotEmpty) "images": images, }; final response = await _postRequest(ApiEndpoints.commentTask, body); diff --git a/lib/helpers/services/auth_service.dart b/lib/helpers/services/auth_service.dart index a9e3c53..af2e593 100644 --- a/lib/helpers/services/auth_service.dart +++ b/lib/helpers/services/auth_service.dart @@ -5,6 +5,7 @@ import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/controller/permission_controller.dart'; import 'package:logger/logger.dart'; import 'package:marco/helpers/services/api_endpoints.dart'; +import 'package:marco/controller/project_controller.dart'; final Logger logger = Logger(); @@ -369,7 +370,8 @@ class AuthService { // ✅ Put and load PermissionController final permissionController = Get.put(PermissionController()); - await permissionController.loadData(jwtToken); + await permissionController.loadData(jwtToken); + await Get.find().fetchProjects(); isLoggedIn = true; } diff --git a/lib/helpers/services/storage/local_storage.dart b/lib/helpers/services/storage/local_storage.dart index e3c0b07..d422131 100644 --- a/lib/helpers/services/storage/local_storage.dart +++ b/lib/helpers/services/storage/local_storage.dart @@ -5,7 +5,9 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:marco/model/user_permission.dart'; import 'package:marco/model/employee_info.dart'; import 'dart:convert'; -import 'package:get/route_manager.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:get/get.dart'; + class LocalStorage { static const String _loggedInUserKey = "user"; @@ -134,20 +136,25 @@ class LocalStorage { return setToken(_refreshTokenKey, refreshToken); } - static Future logout() async { - await removeLoggedInUser(); - await removeToken(_jwtTokenKey); - await removeToken(_refreshTokenKey); - await removeUserPermissions(); - await removeEmployeeInfo(); - await removeMpinToken(); - await removeIsMpin(); - await preferences.remove("mpin_verified"); - await preferences.remove(_languageKey); - await preferences.remove(_themeCustomizerKey); - Get.offAllNamed('/auth/login-option'); +static Future logout() async { + await removeLoggedInUser(); + await removeToken(_jwtTokenKey); + await removeToken(_refreshTokenKey); + await removeUserPermissions(); + await removeEmployeeInfo(); + await removeMpinToken(); + await removeIsMpin(); + await preferences.remove("mpin_verified"); + await preferences.remove(_languageKey); + await preferences.remove(_themeCustomizerKey); + await preferences.remove('selectedProjectId'); + if (Get.isRegistered()) { + Get.find().clearProjects(); } + Get.offAllNamed('/auth/login-option'); +} + static Future setMpinToken(String token) { return preferences.setString(_mpinTokenKey, token); } diff --git a/lib/helpers/widgets/image_viewer_dialog.dart b/lib/helpers/widgets/image_viewer_dialog.dart new file mode 100644 index 0000000..591c56b --- /dev/null +++ b/lib/helpers/widgets/image_viewer_dialog.dart @@ -0,0 +1,122 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; + +class ImageViewerDialog extends StatefulWidget { + final List imageSources; + final int initialIndex; + + const ImageViewerDialog({ + Key? key, + required this.imageSources, + required this.initialIndex, + }) : super(key: key); + + @override + State createState() => _ImageViewerDialogState(); +} + +class _ImageViewerDialogState extends State { + late final PageController _controller; + late int currentIndex; + + bool isFile(dynamic item) => item is File; + + @override + void initState() { + super.initState(); + currentIndex = widget.initialIndex; + _controller = PageController(initialPage: widget.initialIndex); + } + + @override + Widget build(BuildContext context) { + final double dialogHeight = MediaQuery.of(context).size.height * 0.55; + + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 100), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Container( + height: dialogHeight, + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + // Top Close Button + Align( + alignment: Alignment.topRight, + child: IconButton( + icon: const Icon(Icons.close, size: 26), + onPressed: () => Navigator.of(context).pop(), + splashRadius: 22, + tooltip: 'Close', + ), + ), + + // Image Viewer + Expanded( + child: PageView.builder( + controller: _controller, + itemCount: widget.imageSources.length, + onPageChanged: (index) { + setState(() => currentIndex = index); + }, + itemBuilder: (context, index) { + final item = widget.imageSources[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: isFile(item) + ? Image.file(item, fit: BoxFit.contain) + : Image.network( + item, + fit: BoxFit.contain, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + (loadingProgress.expectedTotalBytes ?? 1) + : null, + ), + ); + }, + errorBuilder: (context, error, stackTrace) => + const Center( + child: Icon(Icons.broken_image, + size: 48, color: Colors.grey), + ), + ), + ); + }, + ), + ), + + // Index Indicator + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 12), + child: Text( + '${currentIndex + 1} / ${widget.imageSources.length}', + style: const TextStyle( + color: Colors.black87, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart b/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart index 2c4c435..e4dbf63 100644 --- a/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart +++ b/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart @@ -10,6 +10,7 @@ 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'; +import 'package:marco/helpers/widgets/image_viewer_dialog.dart'; class CommentTaskBottomSheet extends StatefulWidget { final Map taskData; @@ -206,6 +207,148 @@ class _CommentTaskBottomSheetState extends State floatingLabelBehavior: FloatingLabelBehavior.never, ), ), + MySpacing.height(16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.camera_alt_outlined, + size: 18, color: Colors.grey[700]), + MySpacing.width(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleSmall("Attach Photos:", + fontWeight: 600), + MySpacing.height(12), + ], + ), + ), + ], + ), + Obx(() { + final images = controller.selectedImages; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (images.isEmpty) + Container( + height: 70, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.grey.shade300, width: 2), + color: Colors.grey.shade100, + ), + child: Center( + child: Icon(Icons.photo_camera_outlined, + size: 48, color: Colors.grey.shade400), + ), + ) + else + SizedBox( + height: 70, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: images.length, + separatorBuilder: (_, __) => + MySpacing.width(12), + itemBuilder: (context, index) { + final file = images[index]; + return Stack( + children: [ + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (_) => + ImageViewerDialog( + imageSources: images, + initialIndex: index, + ), + ); + }, + child: ClipRRect( + borderRadius: + BorderRadius.circular(12), + child: Image.file( + file, + height: 70, + width: 70, + fit: BoxFit.cover, + ), + ), + ), + Positioned( + top: 4, + right: 4, + child: GestureDetector( + onTap: () => controller + .removeImageAt(index), + child: Container( + decoration: BoxDecoration( + color: Colors.black54, + shape: BoxShape.circle, + ), + child: Icon(Icons.close, + size: 20, + color: Colors.white), + ), + ), + ), + ], + ); + }, + ), + ), + MySpacing.height(16), + Row( + children: [ + Expanded( + child: MyButton.outlined( + onPressed: () => controller.pickImages( + fromCamera: true), + padding: MySpacing.xy(12, 10), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon(Icons.camera_alt, + size: 16, + color: Colors.blueAccent), + MySpacing.width(6), + MyText.bodySmall('Capture', + color: Colors.blueAccent), + ], + ), + ), + ), + MySpacing.width(12), + Expanded( + child: MyButton.outlined( + onPressed: () => controller.pickImages( + fromCamera: false), + padding: MySpacing.xy(12, 10), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon(Icons.upload_file, + size: 16, + color: Colors.blueAccent), + MySpacing.width(6), + MyText.bodySmall('Upload', + color: Colors.blueAccent), + ], + ), + ), + ), + ], + ), + ], + ); + }), MySpacing.height(24), Row( mainAxisAlignment: MainAxisAlignment.end, @@ -233,7 +376,9 @@ class _CommentTaskBottomSheetState extends State .getController('comment') ?.text ?? '', + images: controller.selectedImages, ); + if (widget.onCommentSuccess != null) { widget.onCommentSuccess!(); } @@ -262,12 +407,13 @@ class _CommentTaskBottomSheetState extends State }), ], ), - MySpacing.height(24), + MySpacing.height(10), if ((widget.taskData['taskComments'] as List?) ?.isNotEmpty == true) ...[ Row( children: [ + MySpacing.width(10), Icon(Icons.chat_bubble_outline, size: 18, color: Colors.grey[700]), MySpacing.width(8), @@ -277,6 +423,7 @@ class _CommentTaskBottomSheetState extends State ), ], ), + Divider(), MySpacing.height(12), Builder( builder: (context) { @@ -298,84 +445,236 @@ class _CommentTaskBottomSheetState extends State 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'] ?? ''); + 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, + // Dummy image URLs (simulate as if coming from backend) + final imageUrls = [ + 'https://picsum.photos/seed/${index}a/100', + 'https://picsum.photos/seed/${index}b/100', + 'https://picsum.photos/seed/${index}a/100', + 'https://picsum.photos/seed/${index}b/100', + 'https://picsum.photos/seed/${index}a/100', + 'https://picsum.photos/seed/${index}b/100', + ]; + + return Container( + margin: EdgeInsets.symmetric( + vertical: 0, horizontal: 0), + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: + BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // 🔹 Top Row: Avatar + Name + Time + Row( + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + Avatar( + firstName: commentedBy + .split(' ') + .first, + lastName: commentedBy + .split(' ') + .length > + 1 + ? commentedBy + .split(' ') + .last + : '', + size: 32, + ), + SizedBox(width: 12), + Expanded( + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + MyText.bodyMedium( + commentedBy, + fontWeight: 700, + color: Colors + .black87, + ), + MyText.bodySmall( + relativeTime, + fontSize: 12, + color: Colors + .black54, + ), + ], + ), + ), + ], + ), + + SizedBox(height: 12), + // 🔹 Comment text below attachments + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + MyText.bodyMedium( + commentText, + fontWeight: 500, + color: Colors.black87, + ), + ]), + SizedBox(height: 12), + + // 🔹 Attachments row: full width below top row + if (imageUrls.isNotEmpty) ...[ + Row( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Icon( + Icons + .attach_file_outlined, + size: 18, + color: Colors + .grey[700]), + MyText.bodyMedium( + 'Attachments', + fontWeight: 600, + color: + Colors.black87, + ), + ]), + SizedBox(height: 8), + SizedBox( + height: 60, + child: ListView.separated( + padding: EdgeInsets + .symmetric( + horizontal: 0), + scrollDirection: + Axis.horizontal, + itemCount: + imageUrls.length, + itemBuilder: (context, + imageIndex) { + final imageUrl = + imageUrls[ + imageIndex]; + return GestureDetector( + onTap: () { + showDialog( + context: + context, + barrierColor: + Colors + .black54, + builder: (_) => + ImageViewerDialog( + imageSources: + imageUrls, + initialIndex: + imageIndex, + ), + ); + }, + child: Stack( + children: [ + Container( + width: 60, + height: 60, + decoration: + BoxDecoration( + borderRadius: + BorderRadius.circular( + 12), + color: Colors + .grey[ + 100], + boxShadow: [ + BoxShadow( + color: Colors + .black26, + blurRadius: + 6, + offset: Offset( + 2, + 2), + ), + ], + ), + child: + ClipRRect( + borderRadius: + BorderRadius.circular( + 12), + child: Image + .network( + imageUrl, + fit: BoxFit + .cover, + errorBuilder: (context, + error, + stackTrace) => + Container( + color: Colors + .grey[300], + child: Icon( + Icons + .broken_image, + color: + Colors.grey[700]), + ), + ), + ), + ), + Positioned( + right: 4, + bottom: 4, + child: Icon( + Icons + .zoom_in, + color: Colors + .white70, + size: 16), + ), + ], + ), + ); + }, + separatorBuilder: (_, + __) => + SizedBox(width: 12), ), ), - Text( - relativeTime, - style: TextStyle( - fontSize: 12, - color: Colors.black54, - ), - ) + SizedBox(height: 12), ], - ), - SizedBox(height: 6), - Text( - commentText, - style: TextStyle( - fontWeight: FontWeight.w500, - color: Colors.black87, - ), - ), - ], + ], + ), ), - ), - ], - ), - ); - }, - ), + ], + ), + ); + }), ); }, ), diff --git a/lib/model/dailyTaskPlaning/report_task_bottom_sheet.dart b/lib/model/dailyTaskPlaning/report_task_bottom_sheet.dart index 99441ff..100119d 100644 --- a/lib/model/dailyTaskPlaning/report_task_bottom_sheet.dart +++ b/lib/model/dailyTaskPlaning/report_task_bottom_sheet.dart @@ -10,8 +10,12 @@ import 'package:marco/helpers/widgets/my_text_style.dart'; class ReportTaskBottomSheet extends StatefulWidget { final Map taskData; -final VoidCallback? onReportSuccess; - const ReportTaskBottomSheet({super.key, required this.taskData,this.onReportSuccess,}); + final VoidCallback? onReportSuccess; + const ReportTaskBottomSheet({ + super.key, + required this.taskData, + this.onReportSuccess, + }); @override State createState() => _ReportTaskBottomSheetState(); @@ -201,6 +205,147 @@ class _ReportTaskBottomSheetState extends State ), ), MySpacing.height(24), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.camera_alt_outlined, + size: 18, color: Colors.grey[700]), + MySpacing.width(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleSmall("Attach Photos:", + fontWeight: 600), + MySpacing.height(12), + ], + ), + ), + ], + ), + Obx(() { + final images = controller.selectedImages; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (images.isEmpty) + Container( + height: 70, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.grey.shade300, width: 2), + color: Colors.grey.shade100, + ), + child: Center( + child: Icon(Icons.photo_camera_outlined, + size: 48, color: Colors.grey.shade400), + ), + ) + else + SizedBox( + height: 70, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: images.length, + separatorBuilder: (_, __) => + MySpacing.width(12), + itemBuilder: (context, index) { + final file = images[index]; + return Stack( + children: [ + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (_) => Dialog( + child: InteractiveViewer( + child: Image.file(file), + ), + ), + ); + }, + child: ClipRRect( + borderRadius: + BorderRadius.circular(12), + child: Image.file( + file, + height: 70, + width: 70, + fit: BoxFit.cover, + ), + ), + ), + Positioned( + top: 4, + right: 4, + child: GestureDetector( + onTap: () => controller + .removeImageAt(index), + child: Container( + decoration: BoxDecoration( + color: Colors.black54, + shape: BoxShape.circle, + ), + child: Icon(Icons.close, + size: 20, + color: Colors.white), + ), + ), + ), + ], + ); + }, + ), + ), + MySpacing.height(16), + Row( + children: [ + Expanded( + child: MyButton.outlined( + onPressed: () => controller.pickImages( + fromCamera: true), + padding: MySpacing.xy(12, 10), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon(Icons.camera_alt, + size: 16, + color: Colors.blueAccent), + MySpacing.width(6), + MyText.bodySmall('Capture', + color: Colors.blueAccent), + ], + ), + ), + ), + MySpacing.width(12), + Expanded( + child: MyButton.outlined( + onPressed: () => controller.pickImages( + fromCamera: false), + padding: MySpacing.xy(12, 10), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon(Icons.upload_file, + size: 16, + color: Colors.blueAccent), + MySpacing.width(6), + MyText.bodySmall('Upload', + color: Colors.blueAccent), + ], + ), + ), + ), + ], + ), + ], + ); + }), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -237,6 +382,7 @@ class _ReportTaskBottomSheetState extends State 0, checklist: [], reportedDate: DateTime.now(), + images: controller.selectedImages, ); if (widget.onReportSuccess != null) { widget.onReportSuccess!(); diff --git a/lib/view/auth/mpin_auth_screen.dart b/lib/view/auth/mpin_auth_screen.dart index d680d72..3efb225 100644 --- a/lib/view/auth/mpin_auth_screen.dart +++ b/lib/view/auth/mpin_auth_screen.dart @@ -236,8 +236,17 @@ class _MPINAuthScreenState extends State with UIMixin { ), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], - onChanged: (value) => - controller.onDigitChanged(value, index, isRetype: isRetype), + onChanged: (value) { + controller.onDigitChanged(value, index, isRetype: isRetype); + + if (!isRetype) { + final isComplete = + controller.digitControllers.every((c) => c.text.isNotEmpty); + if (isComplete && !controller.isLoading.value) { + controller.onSubmitMPIN(); + } + } + }, decoration: InputDecoration( counterText: '', filled: true, diff --git a/lib/view/dashboard/Attendence/attendance_screen.dart b/lib/view/dashboard/Attendence/attendance_screen.dart index 29b8ced..eb0f3cc 100644 --- a/lib/view/dashboard/Attendence/attendance_screen.dart +++ b/lib/view/dashboard/Attendence/attendance_screen.dart @@ -36,19 +36,27 @@ class _AttendanceScreenState extends State with UIMixin { @override void initState() { super.initState(); + final projectController = Get.find(); - ever(projectController.selectedProjectId!, (projectId) async { - if (projectId != null && projectId.isNotEmpty) { - try { - await attendanceController.fetchEmployeesByProject(projectId); - await attendanceController.fetchAttendanceLogs(projectId); - await attendanceController.fetchRegularizationLogs(projectId); - await attendanceController.fetchProjectData(projectId); - attendanceController.update(['attendance_dashboard_controller']); - } catch (e) { - debugPrint("Error updating data on project change: $e"); - } - } + final attendanceController = Get.find(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ever( + projectController.selectedProjectId!, + (projectId) async { + if (projectId != null && projectId.isNotEmpty) { + try { + await attendanceController.fetchEmployeesByProject(projectId); + await attendanceController.fetchAttendanceLogs(projectId); + await attendanceController.fetchRegularizationLogs(projectId); + await attendanceController.fetchProjectData(projectId); + attendanceController.update(['attendance_dashboard_controller']); + } catch (e) { + debugPrint("Error updating data on project change: $e"); + } + } + }, + ); }); } @@ -107,6 +115,12 @@ class _AttendanceScreenState extends State with UIMixin { init: attendanceController, tag: 'attendance_dashboard_controller', builder: (controller) { + final selectedProjectId = + Get.find().selectedProjectId?.value; + + final bool noProjectSelected = + selectedProjectId == null || selectedProjectId.isEmpty; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -234,11 +248,22 @@ class _AttendanceScreenState extends State with UIMixin { MyFlex(children: [ MyFlexItem( sizes: 'lg-12 md-12 sm-12', - child: selectedTab == 'todaysAttendance' - ? employeeListTab() - : selectedTab == 'attendanceLogs' - ? employeeLog() - : regularizationScreen(), + child: noProjectSelected + ? Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: MyText.titleMedium( + 'No Records Found', + fontWeight: 600, + color: Colors.grey[600], + ), + ), + ) + : selectedTab == 'todaysAttendance' + ? employeeListTab() + : selectedTab == 'attendanceLogs' + ? employeeLog() + : regularizationScreen(), ), ]), ], diff --git a/lib/view/employees/employees_screen.dart b/lib/view/employees/employees_screen.dart index 33247f0..82370b2 100644 --- a/lib/view/employees/employees_screen.dart +++ b/lib/view/employees/employees_screen.dart @@ -9,12 +9,12 @@ 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/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'; import 'package:marco/controller/project_controller.dart'; + class EmployeesScreen extends StatefulWidget { const EmployeesScreen({super.key}); @@ -27,52 +27,45 @@ class _EmployeesScreenState extends State with UIMixin { Get.put(EmployeesScreenController()); final PermissionController permissionController = Get.put(PermissionController()); + Future _refreshEmployees() async { + try { + final selectedProjectId = + Get.find().selectedProject?.id; + final isAllSelected = + employeeScreenController.isAllEmployeeSelected.value; - 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) { + if (isAllSelected) { + employeeScreenController.selectedProjectId = null; + await employeeScreenController.fetchAllEmployees(); + } else if (selectedProjectId != null) { 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']); + await employeeScreenController + .fetchEmployeesByProject(selectedProjectId); + } else { + // ❗ Clear employees if neither selected + employeeScreenController.clearEmployees(); } + + employeeScreenController.update(['employee_screen_controller']); + } catch (e, stackTrace) { + debugPrint('Error refreshing employee data: ${e.toString()}'); + debugPrintStack(stackTrace: stackTrace); } } - 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 + void initState() { + super.initState(); + final selectedProjectId = Get.find().selectedProject?.id; + + if (selectedProjectId != null) { + employeeScreenController.selectedProjectId = selectedProjectId; + employeeScreenController.fetchEmployeesByProject(selectedProjectId); + } else if (employeeScreenController.isAllEmployeeSelected.value) { + employeeScreenController.selectedProjectId = null; + employeeScreenController.fetchAllEmployees(); + } else { + employeeScreenController.clearEmployees(); } } @@ -194,26 +187,66 @@ class _EmployeesScreenState extends State with UIMixin { 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, + Obx(() { + return Row( + children: [ + Checkbox( + value: employeeScreenController + .isAllEmployeeSelected.value, + activeColor: Colors.blueAccent, + fillColor: + MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.selected)) { + return Colors.blueAccent; + } + return Colors.transparent; + }), + checkColor: Colors.white, + side: BorderSide( + color: Colors.black, + width: 2, ), + onChanged: (value) async { + employeeScreenController + .isAllEmployeeSelected.value = value!; + + if (value) { + employeeScreenController.selectedProjectId = + null; + await employeeScreenController + .fetchAllEmployees(); + } else { + final selectedProjectId = + Get.find() + .selectedProject + ?.id; + + if (selectedProjectId != null) { + employeeScreenController + .selectedProjectId = + selectedProjectId; + await employeeScreenController + .fetchEmployeesByProject( + selectedProjectId); + } else { + // ✅ THIS is your critical path + employeeScreenController.clearEmployees(); + } + } + + employeeScreenController + .update(['employee_screen_controller']); + }, ), - ), - ), - ), - const SizedBox(width: 8), + MyText.bodyMedium( + "All Employees", + fontWeight: 600, + ), + ], + ); + }), + const SizedBox(width: 16), MyText.bodyMedium("Refresh", fontWeight: 600), Tooltip( message: 'Refresh Data', @@ -253,16 +286,17 @@ class _EmployeesScreenState extends State with UIMixin { 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 Padding( + padding: const EdgeInsets.only(top: 50), + child: Center( + child: MyText.bodySmall( + "No Assigned Employees Found", + fontWeight: 600, + ), ), ); } diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index 9da4971..eca8b05 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -100,7 +100,11 @@ class _LayoutState extends State { (p) => p.id == selectedProjectId, ); - if (selectedProject == null && projectController.projects.isNotEmpty) { + final hasProjects = projectController.projects.isNotEmpty; + + if (!hasProjects) { + projectController.selectedProjectId?.value = ''; + } else if (selectedProject == null) { projectController .updateSelectedProject(projectController.projects.first.id); } @@ -111,7 +115,7 @@ class _LayoutState extends State { borderRadius: BorderRadius.circular(12), ), margin: EdgeInsets.zero, - clipBehavior: Clip.antiAlias, // important for overlap inside card + clipBehavior: Clip.antiAlias, child: Stack( children: [ Padding( @@ -129,45 +133,61 @@ class _LayoutState extends State { ), const SizedBox(width: 12), Expanded( - child: GestureDetector( - onTap: () => projectController - .isProjectSelectionExpanded - .toggle(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Row( + child: hasProjects + ? GestureDetector( + onTap: () => projectController + .isProjectSelectionExpanded + .toggle(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ Expanded( - child: MyText.bodyLarge( - selectedProject?.name ?? - "Select Project", - fontWeight: 700, - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: Row( + children: [ + Expanded( + child: MyText.bodyLarge( + selectedProject?.name ?? + "Select Project", + fontWeight: 700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Icon( + isExpanded + ? Icons.arrow_drop_up_outlined + : Icons + .arrow_drop_down_outlined, + color: Colors.black, + ), + ], ), ), - Icon( - isExpanded - ? Icons.arrow_drop_up_outlined - : Icons.arrow_drop_down_outlined, - color: Colors.black, - ), ], ), + MyText.bodyMedium( + "Hi, ${employeeInfo?.firstName ?? ''}", + color: Colors.black54, + ), + ], + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyLarge( + "No Project Assigned", + fontWeight: 700, + color: Colors.redAccent, + ), + MyText.bodyMedium( + "Hi, ${employeeInfo?.firstName ?? ''}", + color: Colors.black54, ), ], ), - MyText.bodyMedium( - "Hi, ${employeeInfo?.firstName ?? ''}", - color: Colors.black54, - ), - ], - ), - ), ), if (isBetaEnvironment) Container( @@ -193,10 +213,10 @@ class _LayoutState extends State { ), ), - /// Expanded Project List inside card - if (isExpanded) + // Expanded Project List inside card — only show if projects exist + if (isExpanded && hasProjects) Positioned( - top: 70, // slightly below the row + top: 70, left: 0, right: 0, child: Container( diff --git a/lib/view/taskPlaning/daily_progress.dart b/lib/view/taskPlaning/daily_progress.dart index aa1a762..d217abc 100644 --- a/lib/view/taskPlaning/daily_progress.dart +++ b/lib/view/taskPlaning/daily_progress.dart @@ -50,17 +50,23 @@ class _DailyProgressReportScreenState extends State dailyTaskController.fetchTaskData(initialProjectId); } - ever( - projectController.selectedProjectId!, - (newProjectId) async { - if (newProjectId != null && - newProjectId != dailyTaskController.selectedProjectId) { - dailyTaskController.selectedProjectId = newProjectId; - await dailyTaskController.fetchTaskData(newProjectId); - dailyTaskController.update(['daily_progress_report_controller']); - } - }, - ); + final selectedProjectIdRx = projectController.selectedProjectId; + if (selectedProjectIdRx != null) { + ever( + selectedProjectIdRx, + (newProjectId) async { + if (newProjectId != null && + newProjectId != dailyTaskController.selectedProjectId) { + dailyTaskController.selectedProjectId = newProjectId; + await dailyTaskController.fetchTaskData(newProjectId); + dailyTaskController.update(['daily_progress_report_controller']); + } + }, + ); + } else { + debugPrint( + "Warning: selectedProjectId is null, skipping listener setup."); + } } @override @@ -133,6 +139,7 @@ class _DailyProgressReportScreenState extends State ), ); } + Widget _buildActionBar() { return Padding( padding: MySpacing.x(flexSpacing), diff --git a/lib/view/taskPlaning/daily_task_planing.dart b/lib/view/taskPlaning/daily_task_planing.dart index 168c107..48997cc 100644 --- a/lib/view/taskPlaning/daily_task_planing.dart +++ b/lib/view/taskPlaning/daily_task_planing.dart @@ -31,18 +31,24 @@ class _DailyTaskPlaningScreenState extends State void initState() { super.initState(); - // Initial fetch + // Initial fetch if a project is already selected final projectId = projectController.selectedProjectId?.value; if (projectId != null) { dailyTaskPlaningController.fetchTaskData(projectId); } // Reactive fetch on project ID change - ever(projectController.selectedProjectId!, (projectId) { - if (projectId != null) { - dailyTaskPlaningController.fetchTaskData(projectId); - } - }); + final selectedProject = projectController.selectedProjectId; + if (selectedProject != null) { + ever( + selectedProject, + (newProjectId) { + if (newProjectId != null) { + dailyTaskPlaningController.fetchTaskData(newProjectId); + } + }, + ); + } } @override diff --git a/lib/view/taskPlaning/report_task_screen.dart b/lib/view/taskPlaning/report_task_screen.dart index 37eb76e..04dc2f8 100644 --- a/lib/view/taskPlaning/report_task_screen.dart +++ b/lib/view/taskPlaning/report_task_screen.dart @@ -182,7 +182,7 @@ class _ReportTaskScreenState extends State with UIMixin { ), ), MySpacing.height(24), - + // Buttons Row( mainAxisAlignment: MainAxisAlignment.end, diff --git a/pubspec.yaml b/pubspec.yaml index 0c536d2..9a804fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: path: ^1.9.0 percent_indicator: ^4.2.2 flutter_contacts: ^1.1.9+2 + photo_view: ^0.15.0 dev_dependencies: flutter_test: sdk: flutter