From 25b20fedda3116e318b4fb3e01f26433f5636f95 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 18 Sep 2025 11:01:03 +0530 Subject: [PATCH 1/8] feat: Enhance dashboard refresh logic; add handling for various notification types and improve method organization --- .../dashboard/dashboard_controller.dart | 47 ++++--- .../services/notification_action_handler.dart | 122 +++++++++++------- 2 files changed, 100 insertions(+), 69 deletions(-) diff --git a/lib/controller/dashboard/dashboard_controller.dart b/lib/controller/dashboard/dashboard_controller.dart index 91d812a..72f2331 100644 --- a/lib/controller/dashboard/dashboard_controller.dart +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -70,9 +70,9 @@ class DashboardController extends GetxController { ever(projectSelectedRange, (_) => fetchProjectProgress()); } - /// ========================= - /// Helper Methods - /// ========================= + // ========================= + // Helper Methods + // ========================= int _getDaysFromRange(String range) { switch (range) { case '7D': @@ -114,21 +114,28 @@ class DashboardController extends GetxController { logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug); } - /// ========================= - /// Manual refresh - /// ========================= + // ========================= + // Manual Refresh Methods + // ========================= Future refreshDashboard() async { logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug); await fetchAllDashboardData(); } - /// ========================= - /// Fetch all dashboard data - /// ========================= + Future refreshAttendance() async => fetchRoleWiseAttendance(); + Future refreshTasks() async { + final projectId = projectController.selectedProjectId.value; + if (projectId.isNotEmpty) await fetchDashboardTasks(projectId: projectId); + } + + Future refreshProjects() async => fetchProjectProgress(); + + // ========================= + // Fetch All Dashboard Data + // ========================= Future fetchAllDashboardData() async { final String projectId = projectController.selectedProjectId.value; - // Skip fetching if no project is selected if (projectId.isEmpty) { logSafe('No project selected. Skipping dashboard API calls.', level: LogLevel.warning); @@ -143,17 +150,15 @@ class DashboardController extends GetxController { ]); } - /// ========================= - /// API Calls - /// ========================= + // ========================= + // API Calls + // ========================= Future fetchRoleWiseAttendance() async { final String projectId = projectController.selectedProjectId.value; - if (projectId.isEmpty) return; try { isAttendanceLoading.value = true; - final List? response = await ApiService.getDashboardAttendanceOverview( projectId, getAttendanceDays()); @@ -179,16 +184,12 @@ class DashboardController extends GetxController { Future fetchProjectProgress() async { final String projectId = projectController.selectedProjectId.value; - if (projectId.isEmpty) return; try { isProjectLoading.value = true; - final response = await ApiService.getProjectProgress( - projectId: projectId, - days: getProjectDays(), - ); + projectId: projectId, days: getProjectDays()); if (response != null && response.success) { projectChartData.value = @@ -208,11 +209,10 @@ class DashboardController extends GetxController { } Future fetchDashboardTasks({required String projectId}) async { - if (projectId.isEmpty) return; // Skip if empty + if (projectId.isEmpty) return; try { isTasksLoading.value = true; - final response = await ApiService.getDashboardTasks(projectId: projectId); if (response != null && response.success) { @@ -235,11 +235,10 @@ class DashboardController extends GetxController { } Future fetchDashboardTeams({required String projectId}) async { - if (projectId.isEmpty) return; // Skip if empty + if (projectId.isEmpty) return; try { isTeamsLoading.value = true; - final response = await ApiService.getDashboardTeams(projectId: projectId); if (response != null && response.success) { diff --git a/lib/helpers/services/notification_action_handler.dart b/lib/helpers/services/notification_action_handler.dart index 4c89f24..a8073f3 100644 --- a/lib/helpers/services/notification_action_handler.dart +++ b/lib/helpers/services/notification_action_handler.dart @@ -9,8 +9,9 @@ import 'package:marco/controller/expense/expense_detail_controller.dart'; import 'package:marco/controller/directory/directory_controller.dart'; import 'package:marco/controller/directory/notes_controller.dart'; import 'package:marco/controller/document/user_document_controller.dart'; -import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/controller/document/document_details_controller.dart'; +import 'package:marco/controller/dashboard/dashboard_controller.dart'; +import 'package:marco/helpers/utils/permission_constants.dart'; /// Handles incoming FCM notification actions and updates UI/controllers. class NotificationActionHandler { @@ -46,6 +47,10 @@ class NotificationActionHandler { break; case 'attendance_updated': _handleAttendanceUpdated(data); + _handleDashboardUpdate(data); // refresh dashboard attendance + break; + case 'dashboard_update': + _handleDashboardUpdate(data); // full dashboard refresh break; default: _logger.w('âš ī¸ Unknown notification type: $type'); @@ -60,16 +65,19 @@ class NotificationActionHandler { case 'Attendance': if (_isAttendanceAction(action)) { _handleAttendanceUpdated(data); + _handleDashboardUpdate(data); } break; /// 🔹 Tasks case 'Report_Task': _handleTaskUpdated(data, isComment: false); + _handleDashboardUpdate(data); break; case 'Task_Comment': _handleTaskUpdated(data, isComment: true); + _handleDashboardUpdate(data); break; case 'Task_Modified': @@ -77,11 +85,13 @@ class NotificationActionHandler { case 'Floor_Modified': case 'Building_Modified': _handleTaskPlanningUpdated(data); + _handleDashboardUpdate(data); break; /// 🔹 Expenses case 'Expenses_Modified': _handleExpenseUpdated(data); + _handleDashboardUpdate(data); break; /// 🔹 Documents @@ -198,59 +208,55 @@ class NotificationActionHandler { } /// ---------------------- DOCUMENT HANDLER ---------------------- - /// ---------------------- DOCUMENT HANDLER ---------------------- -static void _handleDocumentModified(Map data) { - late String entityTypeId; - late String entityId; - String? documentId = data['DocumentId']; + static void _handleDocumentModified(Map data) { + late String entityTypeId; + late String entityId; + String? documentId = data['DocumentId']; - if (data['Keyword'] == 'Employee_Document_Modified') { - entityTypeId = Permissions.employeeEntity; - entityId = data['EmployeeId'] ?? ''; - } else if (data['Keyword'] == 'Project_Document_Modified') { - entityTypeId = Permissions.projectEntity; - entityId = data['ProjectId'] ?? ''; - } else { - _logger.w("âš ī¸ Document update received with unknown keyword: $data"); - return; - } + if (data['Keyword'] == 'Employee_Document_Modified') { + entityTypeId = Permissions.employeeEntity; + entityId = data['EmployeeId'] ?? ''; + } else if (data['Keyword'] == 'Project_Document_Modified') { + entityTypeId = Permissions.projectEntity; + entityId = data['ProjectId'] ?? ''; + } else { + _logger.w("âš ī¸ Document update received with unknown keyword: $data"); + return; + } - if (entityId.isEmpty) { - _logger.w("âš ī¸ Document update missing entityId: $data"); - return; - } + if (entityId.isEmpty) { + _logger.w("âš ī¸ Document update missing entityId: $data"); + return; + } - // 🔹 Refresh document list - _safeControllerUpdate( - onFound: (controller) async { - await controller.fetchDocuments( - entityTypeId: entityTypeId, - entityId: entityId, - reset: true, - ); - }, - notFoundMessage: 'âš ī¸ DocumentController not found, cannot refresh list.', - successMessage: '✅ DocumentController refreshed from notification.', - ); - - // 🔹 Refresh document details (if opened) - if (documentId != null) { - _safeControllerUpdate( + _safeControllerUpdate( onFound: (controller) async { - if (controller.documentDetails.value?.data?.id == documentId) { - await controller.fetchDocumentDetails(documentId); - _logger.i("✅ DocumentDetailsController refreshed for Document $documentId"); - } + await controller.fetchDocuments( + entityTypeId: entityTypeId, + entityId: entityId, + reset: true, + ); }, - notFoundMessage: 'â„šī¸ DocumentDetailsController not active, skipping.', - successMessage: '✅ DocumentDetailsController checked for refresh.', + notFoundMessage: 'âš ī¸ DocumentController not found, cannot refresh list.', + successMessage: '✅ DocumentController refreshed from notification.', ); - } -} + if (documentId != null) { + _safeControllerUpdate( + onFound: (controller) async { + if (controller.documentDetails.value?.data?.id == documentId) { + await controller.fetchDocumentDetails(documentId); + _logger.i( + "✅ DocumentDetailsController refreshed for Document $documentId"); + } + }, + notFoundMessage: 'â„šī¸ DocumentDetailsController not active, skipping.', + successMessage: '✅ DocumentDetailsController checked for refresh.', + ); + } + } /// ---------------------- DIRECTORY HANDLERS ---------------------- - static void _handleContactModified(Map data) { _safeControllerUpdate( onFound: (controller) => controller.fetchContacts(), @@ -296,6 +302,32 @@ static void _handleDocumentModified(Map data) { ); } + /// ---------------------- DASHBOARD HANDLER ---------------------- + static void _handleDashboardUpdate(Map data) { + _safeControllerUpdate( + onFound: (controller) async { + final type = data['type'] ?? ''; + switch (type) { + case 'attendance_updated': + await controller.fetchRoleWiseAttendance(); + break; + case 'task_updated': + await controller.fetchDashboardTasks( + projectId: controller.projectController.selectedProjectId.value); + break; + case 'project_progress_update': + await controller.fetchProjectProgress(); + break; + case 'full_dashboard_refresh': + default: + await controller.refreshDashboard(); + } + }, + notFoundMessage: 'âš ī¸ DashboardController not found, cannot refresh.', + successMessage: '✅ DashboardController refreshed from notification.', + ); + } + /// ---------------------- UTILITY ---------------------- static void _safeControllerUpdate({ From 1fafe77211560a961d4b5ec193e3706ede959e48 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 18 Sep 2025 12:30:44 +0530 Subject: [PATCH 2/8] feat: Enhance document filtering; implement multi-select support and add date range filters --- .../document/user_document_controller.dart | 36 +- .../user_document_filter_bottom_sheet.dart | 402 +++++++++++++++--- lib/view/document/user_document_screen.dart | 14 +- 3 files changed, 373 insertions(+), 79 deletions(-) diff --git a/lib/controller/document/user_document_controller.dart b/lib/controller/document/user_document_controller.dart index c93b5c7..f5f070e 100644 --- a/lib/controller/document/user_document_controller.dart +++ b/lib/controller/document/user_document_controller.dart @@ -10,12 +10,11 @@ class DocumentController extends GetxController { var documents = [].obs; var filters = Rxn(); - // Selected filters - var selectedFilter = "".obs; - var selectedUploadedBy = "".obs; - var selectedCategory = "".obs; - var selectedType = "".obs; - var selectedTag = "".obs; + // ✅ Selected filters (multi-select support) + var selectedUploadedBy = [].obs; + var selectedCategory = [].obs; + var selectedType = [].obs; + var selectedTag = [].obs; // Pagination state var pageNumber = 1.obs; @@ -31,6 +30,11 @@ class DocumentController extends GetxController { // NEW: search var searchQuery = ''.obs; var searchController = TextEditingController(); +// New filter fields + var isUploadedAt = true.obs; + var isVerified = RxnBool(); + var startDate = Rxn(); + var endDate = Rxn(); // ------------------ API Calls ----------------------- @@ -157,17 +161,21 @@ class DocumentController extends GetxController { /// Clear selected filters void clearFilters() { - selectedUploadedBy.value = ""; - selectedCategory.value = ""; - selectedType.value = ""; - selectedTag.value = ""; + selectedUploadedBy.clear(); + selectedCategory.clear(); + selectedType.clear(); + selectedTag.clear(); + isUploadedAt.value = true; + isVerified.value = null; + startDate.value = null; + endDate.value = null; } /// Check if any filters are active (for red dot indicator) bool hasActiveFilters() { - return selectedUploadedBy.value.isNotEmpty || - selectedCategory.value.isNotEmpty || - selectedType.value.isNotEmpty || - selectedTag.value.isNotEmpty; + return selectedUploadedBy.isNotEmpty || + selectedCategory.isNotEmpty || + selectedType.isNotEmpty || + selectedTag.isNotEmpty; } } diff --git a/lib/model/document/user_document_filter_bottom_sheet.dart b/lib/model/document/user_document_filter_bottom_sheet.dart index 0eedf48..d6fa01f 100644 --- a/lib/model/document/user_document_filter_bottom_sheet.dart +++ b/lib/model/document/user_document_filter_bottom_sheet.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/document/user_document_controller.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/utils/date_time_utils.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/model/document/document_filter_model.dart'; +import 'dart:convert'; class UserDocumentFilterBottomSheet extends StatelessWidget { final String entityId; @@ -36,15 +38,21 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { onCancel: () => Get.back(), onSubmit: () { final combinedFilter = { - 'uploadedBy': docController.selectedUploadedBy.value, - 'category': docController.selectedCategory.value, - 'type': docController.selectedType.value, - 'tag': docController.selectedTag.value, + 'uploadedByIds': docController.selectedUploadedBy.toList(), + 'documentCategoryIds': docController.selectedCategory.toList(), + 'documentTypeIds': docController.selectedType.toList(), + 'documentTagIds': docController.selectedTag.toList(), + 'isUploadedAt': docController.isUploadedAt.value, + 'startDate': docController.startDate.value, + 'endDate': docController.endDate.value, + if (docController.isVerified.value != null) + 'isVerified': docController.isVerified.value, }; + docController.fetchDocuments( entityTypeId: entityTypeId, entityId: entityId, - filter: combinedFilter.toString(), + filter: jsonEncode(combinedFilter), reset: true, ); Get.back(); @@ -67,32 +75,209 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { ), ), ), + // --- Date Filter (Uploaded On / Updated On) --- + _buildField( + "Choose Date", + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Segmented Buttons + Obx(() { + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(24), + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => + docController.isUploadedAt.value = true, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 10), + decoration: BoxDecoration( + color: docController.isUploadedAt.value + ? Colors.indigo.shade400 + : Colors.transparent, + borderRadius: + const BorderRadius.horizontal( + left: Radius.circular(24), + ), + ), + child: Center( + child: MyText( + "Uploaded On", + style: MyTextStyle.bodyMedium( + color: + docController.isUploadedAt.value + ? Colors.white + : Colors.black87, + fontWeight: 600, + ), + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () => docController + .isUploadedAt.value = false, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 10), + decoration: BoxDecoration( + color: !docController.isUploadedAt.value + ? Colors.indigo.shade400 + : Colors.transparent, + borderRadius: + const BorderRadius.horizontal( + right: Radius.circular(24), + ), + ), + child: Center( + child: MyText( + "Updated On", + style: MyTextStyle.bodyMedium( + color: !docController + .isUploadedAt.value + ? Colors.white + : Colors.black87, + fontWeight: 600, + ), + ), + ), + ), + ), + ), + ], + ), + ); + }), + MySpacing.height(12), + + // Date Range + Row( + children: [ + Expanded( + child: Obx(() { + return _dateButton( + label: docController.startDate.value == null + ? 'Start Date' + : DateTimeUtils.formatDate( + DateTime.parse( + docController.startDate.value!), + 'dd MMM yyyy', + ), + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime.now(), + ); + if (picked != null) { + docController.startDate.value = + picked.toIso8601String(); + } + }, + ); + }), + ), + MySpacing.width(12), + Expanded( + child: Obx(() { + return _dateButton( + label: docController.endDate.value == null + ? 'End Date' + : DateTimeUtils.formatDate( + DateTime.parse( + docController.endDate.value!), + 'dd MMM yyyy', + ), + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime.now(), + ); + if (picked != null) { + docController.endDate.value = + picked.toIso8601String(); + } + }, + ); + }), + ), + ], + ), + ], + ), + ), + MySpacing.height(8), - _buildDynamicField( + _multiSelectField( label: "Uploaded By", items: filterData.uploadedBy, fallback: "Select Uploaded By", - selectedValue: docController.selectedUploadedBy, + selectedValues: docController.selectedUploadedBy, ), - _buildDynamicField( + _multiSelectField( label: "Category", items: filterData.documentCategory, fallback: "Select Category", - selectedValue: docController.selectedCategory, + selectedValues: docController.selectedCategory, ), - _buildDynamicField( + _multiSelectField( label: "Type", items: filterData.documentType, fallback: "Select Type", - selectedValue: docController.selectedType, + selectedValues: docController.selectedType, ), - _buildDynamicField( + _multiSelectField( label: "Tag", items: filterData.documentTag, fallback: "Select Tag", - selectedValue: docController.selectedTag, + selectedValues: docController.selectedTag, ), - ].where((w) => w != null).cast().toList(), + + // --- Verified Toggle --- + _buildField( + "Only Verified", + Obx(() { + return Container( + padding: MySpacing.xy(12, 6), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText( + "Verified Documents Only", + style: MyTextStyle.bodyMedium(), + ), + Switch( + value: docController.isVerified.value ?? false, + onChanged: (val) { + docController.isVerified.value = + val ? true : null; + }, + activeColor: Colors.indigo, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + ], + ), + ); + }), + ), + ], ) : Center( child: Padding( @@ -110,70 +295,161 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { ); } - Widget? _buildDynamicField({ + Widget _multiSelectField({ required String label, required List items, required String fallback, - required RxString selectedValue, + required RxList selectedValues, }) { - if (items.isEmpty) return null; + if (items.isEmpty) return const SizedBox.shrink(); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelMedium(label), MySpacing.height(8), - _popupSelector(items, fallback, selectedValue: selectedValue), + Obx(() { + final selectedNames = items + .where((f) => selectedValues.contains(f.id)) + .map((f) => f.name) + .join(", "); + final displayText = + selectedNames.isNotEmpty ? selectedNames : fallback; + + return Builder( + builder: (context) { + return GestureDetector( + onTap: () async { + final RenderBox button = + context.findRenderObject() as RenderBox; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + + final position = button.localToGlobal(Offset.zero); + + await showMenu( + context: context, + position: RelativeRect.fromLTRB( + position.dx, + position.dy + button.size.height, + overlay.size.width - position.dx - button.size.width, + 0, + ), + items: items.map( + (f) { + return PopupMenuItem( + enabled: false, // prevent auto-close + child: StatefulBuilder( + builder: (context, setState) { + final isChecked = selectedValues.contains(f.id); + return CheckboxListTile( + dense: true, + value: isChecked, + contentPadding: EdgeInsets.zero, + controlAffinity: + ListTileControlAffinity.leading, + title: MyText(f.name), + + // --- Styles --- + checkColor: Colors.white, // tick color + side: const BorderSide( + color: Colors.black, + width: 1.5), // border when unchecked + + fillColor: + MaterialStateProperty.resolveWith( + (states) { + if (states + .contains(MaterialState.selected)) { + return Colors.indigo; // checked → Indigo + } + return Colors.white; // unchecked → White + }, + ), + + onChanged: (val) { + if (val == true) { + selectedValues.add(f.id); + } else { + selectedValues.remove(f.id); + } + setState(() {}); // refresh UI + }, + ); + }, + ), + ); + }, + ).toList(), + ); + }, + child: Container( + padding: MySpacing.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: MyText( + displayText, + style: const TextStyle(color: Colors.black87), + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), + ), + ); + }, + ); + }), MySpacing.height(16), ], ); } - Widget _popupSelector( - List items, - String fallback, { - required RxString selectedValue, - }) { - return Obx(() { - final currentValue = _getCurrentName(selectedValue.value, items, fallback); - return PopupMenuButton( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - onSelected: (val) => selectedValue.value = val, - itemBuilder: (context) => items - .map( - (f) => PopupMenuItem( - value: f.id, - child: MyText(f.name), - ), - ) - .toList(), - child: Container( - padding: MySpacing.all(12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: MyText( - currentValue, - style: const TextStyle(color: Colors.black87), - overflow: TextOverflow.ellipsis, - ), - ), - const Icon(Icons.arrow_drop_down, color: Colors.grey), - ], - ), - ), - ); - }); + Widget _buildField(String label, Widget child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium(label), + MySpacing.height(8), + child, + MySpacing.height(8), + ], + ); } - String _getCurrentName(String selectedId, List list, String fallback) { - if (selectedId.isEmpty) return fallback; - final match = list.firstWhereOrNull((f) => f.id == selectedId); - return match?.name ?? fallback; + Widget _dateButton({required String label, required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: MySpacing.xy(16, 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + children: [ + const Icon(Icons.calendar_today, size: 16, color: Colors.grey), + MySpacing.width(8), + Expanded( + child: MyText( + label, + style: MyTextStyle.bodyMedium(), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); } } diff --git a/lib/view/document/user_document_screen.dart b/lib/view/document/user_document_screen.dart index a33651e..dadada1 100644 --- a/lib/view/document/user_document_screen.dart +++ b/lib/view/document/user_document_screen.dart @@ -17,6 +17,8 @@ import 'package:marco/helpers/widgets/custom_app_bar.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/controller/permission_controller.dart'; +import 'dart:convert'; + class UserDocumentsPage extends StatefulWidget { final String? entityId; @@ -504,10 +506,18 @@ class _UserDocumentsPageState extends State { Expanded( child: MyRefreshIndicator( onRefresh: () async { + final combinedFilter = { + 'uploadedByIds': docController.selectedUploadedBy.toList(), + 'documentCategoryIds': + docController.selectedCategory.toList(), + 'documentTypeIds': docController.selectedType.toList(), + 'documentTagIds': docController.selectedTag.toList(), + }; + await docController.fetchDocuments( entityTypeId: entityTypeId, entityId: resolvedEntityId, - filter: docController.selectedFilter.value, + filter: jsonEncode(combinedFilter), reset: true, ); }, @@ -577,7 +587,7 @@ class _UserDocumentsPageState extends State { backgroundColor: Colors.transparent, builder: (_) => DocumentUploadBottomSheet( isEmployee: - widget.isEmployee, // 👈 Pass the employee flag here + widget.isEmployee, onSubmit: (data) async { final success = await uploadController.uploadDocument( name: data["name"], From e6f028d12977df9e637400389653fccaac6e6b54 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 18 Sep 2025 15:16:53 +0530 Subject: [PATCH 3/8] feat: Improve notification handling; enhance logging and ensure DocumentController registration before updates --- .../services/notification_action_handler.dart | 57 ++++++++++++------- lib/view/document/document_details_page.dart | 3 +- lib/view/document/user_document_screen.dart | 8 +-- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/lib/helpers/services/notification_action_handler.dart b/lib/helpers/services/notification_action_handler.dart index a8073f3..776786e 100644 --- a/lib/helpers/services/notification_action_handler.dart +++ b/lib/helpers/services/notification_action_handler.dart @@ -209,10 +209,11 @@ class NotificationActionHandler { /// ---------------------- DOCUMENT HANDLER ---------------------- static void _handleDocumentModified(Map data) { - late String entityTypeId; - late String entityId; + String entityTypeId; + String entityId; String? documentId = data['DocumentId']; + // Determine entity type and ID if (data['Keyword'] == 'Employee_Document_Modified') { entityTypeId = Permissions.employeeEntity; entityId = data['EmployeeId'] ?? ''; @@ -229,30 +230,43 @@ class NotificationActionHandler { return; } - _safeControllerUpdate( - onFound: (controller) async { - await controller.fetchDocuments( - entityTypeId: entityTypeId, - entityId: entityId, - reset: true, - ); - }, - notFoundMessage: 'âš ī¸ DocumentController not found, cannot refresh list.', - successMessage: '✅ DocumentController refreshed from notification.', - ); + _logger.i( + "🔔 Document notification received: keyword=${data['Keyword']}, entityTypeId=$entityTypeId, entityId=$entityId, documentId=$documentId"); - if (documentId != null) { + // Refresh Document List + if (Get.isRegistered()) { + _safeControllerUpdate( + onFound: (controller) async { + await controller.fetchDocuments( + entityTypeId: entityTypeId, + entityId: entityId, + reset: true, + ); + }, + notFoundMessage: + 'âš ī¸ DocumentController not found, cannot refresh list.', + successMessage: '✅ DocumentController refreshed from notification.', + ); + } else { + _logger.w('âš ī¸ DocumentController not registered, skipping list refresh.'); + } + + // Refresh Document Details (if open) + if (documentId != null && Get.isRegistered()) { _safeControllerUpdate( onFound: (controller) async { - if (controller.documentDetails.value?.data?.id == documentId) { - await controller.fetchDocumentDetails(documentId); - _logger.i( - "✅ DocumentDetailsController refreshed for Document $documentId"); - } + // Refresh details regardless of current document + await controller.fetchDocumentDetails(documentId); + _logger.i( + "✅ DocumentDetailsController refreshed for Document $documentId"); }, - notFoundMessage: 'â„šī¸ DocumentDetailsController not active, skipping.', + notFoundMessage: + 'â„šī¸ DocumentDetailsController not active, skipping details refresh.', successMessage: '✅ DocumentDetailsController checked for refresh.', ); + } else if (documentId != null) { + _logger.w( + 'âš ī¸ DocumentDetailsController not registered, cannot refresh document details.'); } } @@ -313,7 +327,8 @@ class NotificationActionHandler { break; case 'task_updated': await controller.fetchDashboardTasks( - projectId: controller.projectController.selectedProjectId.value); + projectId: + controller.projectController.selectedProjectId.value); break; case 'project_progress_update': await controller.fetchProjectProgress(); diff --git a/lib/view/document/document_details_page.dart b/lib/view/document/document_details_page.dart index b931844..d84481a 100644 --- a/lib/view/document/document_details_page.dart +++ b/lib/view/document/document_details_page.dart @@ -25,7 +25,8 @@ class DocumentDetailsPage extends StatefulWidget { class _DocumentDetailsPageState extends State { final DocumentDetailsController controller = - Get.put(DocumentDetailsController()); + Get.find(); + final PermissionController permissionController = Get.find(); @override diff --git a/lib/view/document/user_document_screen.dart b/lib/view/document/user_document_screen.dart index dadada1..caafcf3 100644 --- a/lib/view/document/user_document_screen.dart +++ b/lib/view/document/user_document_screen.dart @@ -17,9 +17,9 @@ import 'package:marco/helpers/widgets/custom_app_bar.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/controller/document/document_details_controller.dart'; import 'dart:convert'; - class UserDocumentsPage extends StatefulWidget { final String? entityId; final bool isEmployee; @@ -38,7 +38,8 @@ class _UserDocumentsPageState extends State { final DocumentController docController = Get.put(DocumentController()); final PermissionController permissionController = Get.find(); - + final DocumentDetailsController controller = + Get.put(DocumentDetailsController()); String get entityTypeId => widget.isEmployee ? Permissions.employeeEntity : Permissions.projectEntity; @@ -586,8 +587,7 @@ class _UserDocumentsPageState extends State { isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => DocumentUploadBottomSheet( - isEmployee: - widget.isEmployee, + isEmployee: widget.isEmployee, onSubmit: (data) async { final success = await uploadController.uploadDocument( name: data["name"], From 47666c789775896897debcce20a6f8418a485144 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 18 Sep 2025 16:18:01 +0530 Subject: [PATCH 4/8] feat: Enhance contact detail screen; implement reactive contact updates and improve note handling --- .../services/notification_action_handler.dart | 29 ++++++----- lib/view/directory/contact_detail_screen.dart | 48 ++++++++++--------- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/lib/helpers/services/notification_action_handler.dart b/lib/helpers/services/notification_action_handler.dart index 776786e..a84f4ee 100644 --- a/lib/helpers/services/notification_action_handler.dart +++ b/lib/helpers/services/notification_action_handler.dart @@ -270,36 +270,41 @@ class NotificationActionHandler { } } + /// ---------------------- DIRECTORY HANDLERS ---------------------- /// ---------------------- DIRECTORY HANDLERS ---------------------- static void _handleContactModified(Map data) { - _safeControllerUpdate( - onFound: (controller) => controller.fetchContacts(), - notFoundMessage: 'âš ī¸ DirectoryController not found, cannot refresh.', - successMessage: '✅ Directory contacts refreshed from notification.', - ); - } - - static void _handleContactNoteModified(Map data) { - final contactId = data['contactId']; + final contactId = data['ContactId']; + // Always refresh the contact list _safeControllerUpdate( onFound: (controller) { + controller.fetchContacts(); + // If a specific contact is provided, refresh its notes as well if (contactId != null) { controller.fetchCommentsForContact(contactId); } }, notFoundMessage: - 'âš ī¸ DirectoryController not found, cannot refresh notes.', - successMessage: '✅ Directory comments refreshed from notification.', + 'âš ī¸ DirectoryController not found, cannot refresh contacts.', + successMessage: + '✅ Directory contacts (and notes if applicable) refreshed from notification.', ); + // Refresh notes globally as well _safeControllerUpdate( onFound: (controller) => controller.fetchNotes(), - notFoundMessage: 'âš ī¸ NotesController not found, cannot refresh.', + notFoundMessage: 'âš ī¸ NotesController not found, cannot refresh notes.', successMessage: '✅ Notes refreshed from notification.', ); } + static void _handleContactNoteModified(Map data) { + final contactId = data['ContactId']; + + // Refresh both contacts and notes when a note is modified + _handleContactModified(data); + } + static void _handleBucketModified(Map data) { _safeControllerUpdate( onFound: (controller) => controller.fetchBuckets(), diff --git a/lib/view/directory/contact_detail_screen.dart b/lib/view/directory/contact_detail_screen.dart index bba51d7..b00856b 100644 --- a/lib/view/directory/contact_detail_screen.dart +++ b/lib/view/directory/contact_detail_screen.dart @@ -63,6 +63,7 @@ String _convertDeltaToHtml(dynamic delta) { class ContactDetailScreen extends StatefulWidget { final ContactModel contact; const ContactDetailScreen({super.key, required this.contact}); + @override State createState() => _ContactDetailScreenState(); } @@ -70,16 +71,25 @@ class ContactDetailScreen extends StatefulWidget { class _ContactDetailScreenState extends State { late final DirectoryController directoryController; late final ProjectController projectController; - late ContactModel contact; + + late Rx contactRx; @override void initState() { super.initState(); directoryController = Get.find(); projectController = Get.find(); - contact = widget.contact; + contactRx = widget.contact.obs; + WidgetsBinding.instance.addPostFrameCallback((_) { - directoryController.fetchCommentsForContact(contact.id); + directoryController.fetchCommentsForContact(contactRx.value.id); + }); + + // Listen to controller's allContacts and update contact if changed + ever(directoryController.allContacts, (_) { + final updated = directoryController.allContacts + .firstWhereOrNull((c) => c.id == contactRx.value.id); + if (updated != null) contactRx.value = updated; }); } @@ -94,12 +104,12 @@ class _ContactDetailScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSubHeader(), + Obx(() => _buildSubHeader(contactRx.value)), const Divider(height: 1, thickness: 0.5, color: Colors.grey), Expanded( child: TabBarView(children: [ - _buildDetailsTab(), - _buildCommentsTab(context), + Obx(() => _buildDetailsTab(contactRx.value)), + _buildCommentsTab(), ]), ), ], @@ -135,9 +145,9 @@ class _ContactDetailScreenState extends State { MyText.titleLarge('Contact Profile', fontWeight: 700, color: Colors.black), MySpacing.height(2), - GetBuilder( - builder: (p) => ProjectLabel(p.selectedProject?.name), - ), + GetBuilder(builder: (p) { + return ProjectLabel(p.selectedProject?.name); + }), ], ), ), @@ -147,7 +157,7 @@ class _ContactDetailScreenState extends State { ); } - Widget _buildSubHeader() { + Widget _buildSubHeader(ContactModel contact) { final firstName = contact.name.split(" ").first; final lastName = contact.name.split(" ").length > 1 ? contact.name.split(" ").last : ""; @@ -196,7 +206,7 @@ class _ContactDetailScreenState extends State { ); } - Widget _buildDetailsTab() { + Widget _buildDetailsTab(ContactModel contact) { final tags = contact.tags.map((e) => e.name).join(", "); final bucketNames = contact.bucketIds .map((id) => directoryController.contactBuckets @@ -249,7 +259,6 @@ class _ContactDetailScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ MySpacing.height(12), - // BASIC INFO CARD _infoCard("Basic Info", [ multiRows( items: @@ -273,20 +282,17 @@ class _ContactDetailScreenState extends State { ), _iconInfoRow(Icons.location_on, "Address", contact.address), ]), - // ORGANIZATION CARD _infoCard("Organization", [ _iconInfoRow( Icons.business, "Organization", contact.organization), _iconInfoRow(Icons.category, "Category", category), ]), - // META INFO CARD _infoCard("Meta Info", [ _iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"), _iconInfoRow(Icons.folder_shared, "Contact Buckets", bucketNames.isNotEmpty ? bucketNames : "-"), _iconInfoRow(Icons.work_outline, "Projects", projectNames), ]), - // DESCRIPTION CARD _infoCard("Description", [ MySpacing.height(6), Align( @@ -318,7 +324,7 @@ class _ContactDetailScreenState extends State { final updated = directoryController.allContacts .firstWhereOrNull((c) => c.id == contact.id); if (updated != null) { - setState(() => contact = updated); + contactRx.value = updated; } } }, @@ -331,9 +337,9 @@ class _ContactDetailScreenState extends State { ); } - Widget _buildCommentsTab(BuildContext context) { + Widget _buildCommentsTab() { return Obx(() { - final contactId = contact.id; + final contactId = contactRx.value.id; if (!directoryController.contactCommentsMap.containsKey(contactId)) { return const Center(child: CircularProgressIndicator()); } @@ -355,7 +361,7 @@ class _ContactDetailScreenState extends State { physics: const AlwaysScrollableScrollPhysics(), children: [ SizedBox( - height: MediaQuery.of(context).size.height * 0.6, + height: Get.height * 0.6, child: Center( child: MyText.bodyLarge( "No comments yet.", @@ -375,7 +381,7 @@ class _ContactDetailScreenState extends State { itemBuilder: (_, index) => _buildCommentItem( comments[index], editingId, - contact.id, + contactId, ), ), ), @@ -438,7 +444,6 @@ class _ContactDetailScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header Row Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -474,7 +479,6 @@ class _ContactDetailScreenState extends State { ), ], ), - // Comment Content if (isEditing && quillController != null) CommentEditorCard( controller: quillController, From a1cd212e74340747517322ce6f26f73be88ab81e Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 18 Sep 2025 16:32:49 +0530 Subject: [PATCH 5/8] style: Improve code formatting; enhance readability by adjusting line breaks and widget dimensions --- .../widgets/expense/expense_form_widgets.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/helpers/widgets/expense/expense_form_widgets.dart b/lib/helpers/widgets/expense/expense_form_widgets.dart index 027b82f..e921683 100644 --- a/lib/helpers/widgets/expense/expense_form_widgets.dart +++ b/lib/helpers/widgets/expense/expense_form_widgets.dart @@ -154,8 +154,10 @@ class TileContainer extends StatelessWidget { const TileContainer({required this.child, Key? key}) : super(key: key); @override - Widget build(BuildContext context) => - Container(padding: const EdgeInsets.all(14), decoration: _tileDecoration, child: child); + Widget build(BuildContext context) => Container( + padding: const EdgeInsets.all(14), + decoration: _tileDecoration, + child: child); } /// ========================== @@ -187,9 +189,8 @@ class AttachmentsSection extends StatelessWidget { @override Widget build(BuildContext context) { return Obx(() { - final activeExisting = existingAttachments - .where((doc) => doc['isActive'] != false) - .toList(); + final activeExisting = + existingAttachments.where((doc) => doc['isActive'] != false).toList(); final imageFiles = attachments.where(_isImageFile).toList(); final imageExisting = activeExisting @@ -336,8 +337,8 @@ class AttachmentsSection extends StatelessWidget { Widget _buildActionTile(IconData icon, VoidCallback onTap) => GestureDetector( onTap: onTap, child: Container( - width: 80, - height: 80, + width: 50, + height: 50, decoration: _tileDecoration.copyWith( border: Border.all(color: Colors.grey.shade400), ), @@ -359,7 +360,8 @@ class _AttachmentTile extends StatelessWidget { Widget build(BuildContext context) { final fileName = file.path.split('/').last; final extension = fileName.split('.').last.toLowerCase(); - final isImage = AttachmentsSection.allowedImageExtensions.contains(extension); + final isImage = + AttachmentsSection.allowedImageExtensions.contains(extension); final (icon, color) = _fileIcon(extension); From 957bae526f29fb7c5aa03b2f2cb0898e5e009e37 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 18 Sep 2025 16:36:05 +0530 Subject: [PATCH 6/8] feat: Increase icon size in directory view for better visibility --- lib/view/directory/directory_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/view/directory/directory_view.dart b/lib/view/directory/directory_view.dart index 9f1a290..35fca78 100644 --- a/lib/view/directory/directory_view.dart +++ b/lib/view/directory/directory_view.dart @@ -591,7 +591,7 @@ class _DirectoryViewState extends State { FontAwesomeIcons .whatsapp, color: Colors.green, - size: 16, + size: 25, ), ), ], From 544eb4dc791c5ae4c12fcc0bc01e91d134969053 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 18 Sep 2025 17:15:23 +0530 Subject: [PATCH 7/8] feat: Add todaysAssigned field to WorkItem model and implement JSON parsing --- lib/model/dailyTaskPlanning/daily_task_planning_model.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/model/dailyTaskPlanning/daily_task_planning_model.dart b/lib/model/dailyTaskPlanning/daily_task_planning_model.dart index f1e5e64..1adf51c 100644 --- a/lib/model/dailyTaskPlanning/daily_task_planning_model.dart +++ b/lib/model/dailyTaskPlanning/daily_task_planning_model.dart @@ -129,6 +129,7 @@ class WorkItem { final WorkCategoryMaster? workCategoryMaster; final double? plannedWork; final double? completedWork; + final double? todaysAssigned; final DateTime? taskDate; final String? tenantId; final Tenant? tenant; @@ -143,6 +144,7 @@ class WorkItem { this.workCategoryMaster, this.plannedWork, this.completedWork, + this.todaysAssigned, this.taskDate, this.tenantId, this.tenant, @@ -171,6 +173,9 @@ class WorkItem { completedWork: json['completedWork'] != null ? (json['completedWork'] as num).toDouble() : null, + todaysAssigned: json['todaysAssigned'] != null + ? (json['todaysAssigned'] as num).toDouble() + : null, // ✅ added parsing taskDate: json['taskDate'] != null ? DateTime.tryParse(json['taskDate']) : null, tenantId: json['tenantId'] as String?, From 4836dd994ce2df9cf445acd5d13178f43fa6542d Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 18 Sep 2025 17:42:27 +0530 Subject: [PATCH 8/8] feat: Implement employee editing functionality; add prefill logic and update API service for createOrUpdateEmployee --- .../employee/add_employee_controller.dart | 64 +++++++++++--- lib/helpers/services/api_service.dart | 6 +- .../employees/add_employee_bottom_sheet.dart | 85 ++++++------------- .../employees/employee_detail_screen.dart | 36 +++++++- 4 files changed, 114 insertions(+), 77 deletions(-) diff --git a/lib/controller/employee/add_employee_controller.dart b/lib/controller/employee/add_employee_controller.dart index b23e842..4d51351 100644 --- a/lib/controller/employee/add_employee_controller.dart +++ b/lib/controller/employee/add_employee_controller.dart @@ -7,6 +7,7 @@ import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:marco/helpers/services/app_logger.dart'; +import 'package:collection/collection.dart'; enum Gender { male, @@ -17,6 +18,8 @@ enum Gender { } class AddEmployeeController extends MyController { + Map? editingEmployeeData; // For edit mode + List files = []; final MyFormValidator basicValidator = MyFormValidator(); Gender? selectedGender; @@ -33,12 +36,10 @@ class AddEmployeeController extends MyController { logSafe("Initializing AddEmployeeController..."); _initializeFields(); fetchRoles(); - } - void setJoiningDate(DateTime date) { - joiningDate = date; - logSafe("Joining date selected: $date"); - update(); + if (editingEmployeeData != null) { + prefillFields(); + } } void _initializeFields() { @@ -63,6 +64,37 @@ class AddEmployeeController extends MyController { logSafe("Fields initialized for first_name, phone_number, last_name."); } + /// Prefill fields in edit mode + // In AddEmployeeController + void prefillFields() { + logSafe("Prefilling data for editing..."); + basicValidator.getController('first_name')?.text = + editingEmployeeData?['first_name'] ?? ''; + basicValidator.getController('last_name')?.text = + editingEmployeeData?['last_name'] ?? ''; + basicValidator.getController('phone_number')?.text = + editingEmployeeData?['phone_number'] ?? ''; + + selectedGender = editingEmployeeData?['gender'] != null + ? Gender.values + .firstWhereOrNull((g) => g.name == editingEmployeeData!['gender']) + : null; + + selectedRoleId = editingEmployeeData?['job_role_id']; + + if (editingEmployeeData?['joining_date'] != null) { + joiningDate = DateTime.tryParse(editingEmployeeData!['joining_date']); + } + + update(); + } + + void setJoiningDate(DateTime date) { + joiningDate = date; + logSafe("Joining date selected: $date"); + update(); + } + void onGenderSelected(Gender? gender) { selectedGender = gender; logSafe("Gender selected: ${gender?.name}"); @@ -92,10 +124,13 @@ class AddEmployeeController extends MyController { update(); } - Future?> createEmployees() async { - logSafe("Starting employee creation..."); + /// Create or update employee + Future?> createOrUpdateEmployee() async { + logSafe(editingEmployeeData != null + ? "Starting employee update..." + : "Starting employee creation..."); + if (selectedGender == null || selectedRoleId == null) { - logSafe("Missing gender or role.", level: LogLevel.warning); showAppSnackbar( title: "Missing Fields", message: "Please select both Gender and Role.", @@ -111,6 +146,7 @@ class AddEmployeeController extends MyController { try { final response = await ApiService.createEmployee( + id: editingEmployeeData?['id'], // Pass id if editing firstName: firstName!, lastName: lastName!, phoneNumber: phoneNumber!, @@ -122,25 +158,25 @@ class AddEmployeeController extends MyController { logSafe("Response: $response"); if (response != null && response['success'] == true) { - logSafe("Employee created successfully."); showAppSnackbar( title: "Success", - message: "Employee created successfully!", + message: editingEmployeeData != null + ? "Employee updated successfully!" + : "Employee created successfully!", type: SnackbarType.success, ); return response; } else { - logSafe("Failed to create employee (response false)", - level: LogLevel.error); + logSafe("Failed operation", level: LogLevel.error); } } catch (e, st) { - logSafe("Error creating employee", + logSafe("Error creating/updating employee", level: LogLevel.error, error: e, stackTrace: st); } showAppSnackbar( title: "Error", - message: "Failed to create employee.", + message: "Failed to save employee.", type: SnackbarType.error, ); return null; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index d0f12e2..68f58c9 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -1880,6 +1880,7 @@ class ApiService { _getRequest(ApiEndpoints.getRoles).then( (res) => res != null ? _parseResponse(res, label: 'Roles') : null); static Future?> createEmployee({ + String? id, // Optional, for editing required String firstName, required String lastName, required String phoneNumber, @@ -1888,12 +1889,13 @@ class ApiService { required String joiningDate, }) async { final body = { + if (id != null) "id": id, // Include id only if editing "firstName": firstName, "lastName": lastName, "phoneNumber": phoneNumber, "gender": gender, "jobRoleId": jobRoleId, - "joiningDate": joiningDate + "joiningDate": joiningDate, }; final response = await _postRequest( @@ -1907,7 +1909,7 @@ class ApiService { final json = jsonDecode(response.body); return { "success": response.statusCode == 200 && json['success'] == true, - "data": json + "data": json, }; } diff --git a/lib/model/employees/add_employee_bottom_sheet.dart b/lib/model/employees/add_employee_bottom_sheet.dart index a59d160..a4cc09a 100644 --- a/lib/model/employees/add_employee_bottom_sheet.dart +++ b/lib/model/employees/add_employee_bottom_sheet.dart @@ -11,15 +11,31 @@ import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:intl/intl.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; - class AddEmployeeBottomSheet extends StatefulWidget { + final Map? employeeData; + AddEmployeeBottomSheet({this.employeeData}); + @override State createState() => _AddEmployeeBottomSheetState(); } class _AddEmployeeBottomSheetState extends State with UIMixin { - final AddEmployeeController _controller = Get.put(AddEmployeeController()); + late final AddEmployeeController _controller; + + @override + void initState() { + super.initState(); + _controller = Get.put( + AddEmployeeController(), + tag: UniqueKey().toString(), + ); + + if (widget.employeeData != null) { + _controller.editingEmployeeData = widget.employeeData; + _controller.prefillFields(); + } + } @override Widget build(BuildContext context) { @@ -27,7 +43,7 @@ class _AddEmployeeBottomSheetState extends State init: _controller, builder: (_) { return BaseBottomSheet( - title: "Add Employee", + title: widget.employeeData != null ? "Edit Employee" : "Add Employee", onCancel: () => Navigator.pop(context), onSubmit: _handleSubmit, child: Form( @@ -98,7 +114,6 @@ class _AddEmployeeBottomSheetState extends State ); } - // --- Common label with red star --- Widget _requiredLabel(String text) { return Row( children: [ @@ -109,7 +124,6 @@ class _AddEmployeeBottomSheetState extends State ); } - // --- Date Picker field --- Widget _buildDatePickerField({ required String label, required String value, @@ -146,7 +160,7 @@ class _AddEmployeeBottomSheetState extends State Future _pickJoiningDate(BuildContext context) async { final picked = await showDatePicker( context: context, - initialDate: DateTime.now(), + initialDate: _controller.joiningDate ?? DateTime.now(), firstDate: DateTime(2000), lastDate: DateTime(2100), ); @@ -157,54 +171,25 @@ class _AddEmployeeBottomSheetState extends State } } - // --- Submit logic --- Future _handleSubmit() async { - // Run form validation first final isValid = _controller.basicValidator.formKey.currentState?.validate() ?? false; - if (!isValid) { + if (!isValid || + _controller.joiningDate == null || + _controller.selectedGender == null || + _controller.selectedRoleId == null) { showAppSnackbar( title: "Missing Fields", - message: "Please fill all required fields before submitting.", + message: "Please complete all required fields.", type: SnackbarType.warning, ); return; } - // Additional check for dropdowns & joining date - if (_controller.joiningDate == null) { - showAppSnackbar( - title: "Missing Fields", - message: "Please select Joining Date.", - type: SnackbarType.warning, - ); - return; - } - - if (_controller.selectedGender == null) { - showAppSnackbar( - title: "Missing Fields", - message: "Please select Gender.", - type: SnackbarType.warning, - ); - return; - } - - if (_controller.selectedRoleId == null) { - showAppSnackbar( - title: "Missing Fields", - message: "Please select Role.", - type: SnackbarType.warning, - ); - return; - } - - // All validations passed → Call API - final result = await _controller.createEmployees(); + final result = await _controller.createOrUpdateEmployee(); if (result != null && result['success'] == true) { - final employeeData = result['data']; final employeeController = Get.find(); final projectId = employeeController.selectedProjectId; @@ -216,20 +201,10 @@ class _AddEmployeeBottomSheetState extends State employeeController.update(['employee_screen_controller']); - // Reset 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.joiningDate = null; - _controller.update(); - - Navigator.pop(context, employeeData); + Navigator.pop(context, result['data']); } } - // --- Section label widget --- Widget _sectionLabel(String title) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -239,7 +214,6 @@ class _AddEmployeeBottomSheetState extends State ], ); - // --- Input field with icon --- Widget _inputWithIcon({ required String label, required String hint, @@ -268,7 +242,6 @@ class _AddEmployeeBottomSheetState extends State ); } - // --- Phone input --- Widget _buildPhoneInput(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -322,7 +295,6 @@ class _AddEmployeeBottomSheetState extends State ); } - // --- Dropdown (Gender/Role) --- Widget _buildDropdownField({ required String label, required String value, @@ -356,7 +328,6 @@ class _AddEmployeeBottomSheetState extends State ); } - // --- Common input decoration --- InputDecoration _inputDecoration(String hint) { return InputDecoration( hintText: hint, @@ -379,7 +350,6 @@ class _AddEmployeeBottomSheetState extends State ); } - // --- Gender popup --- void _showGenderPopup(BuildContext context) async { final selected = await showMenu( context: context, @@ -398,7 +368,6 @@ class _AddEmployeeBottomSheetState extends State } } - // --- Role popup --- void _showRolePopup(BuildContext context) async { final selected = await showMenu( context: context, diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index ffd1a63..bcf0098 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -11,6 +11,7 @@ import 'package:marco/helpers/utils/launcher_utils.dart'; import 'package:marco/controller/permission_controller.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; +import 'package:marco/model/employees/add_employee_bottom_sheet.dart'; class EmployeeDetailPage extends StatefulWidget { final String employeeId; @@ -92,8 +93,9 @@ class _EmployeeDetailPageState extends State { fontWeight: FontWeight.normal, color: (isEmail || isPhone) ? Colors.indigo : Colors.black54, fontSize: 14, - decoration: - (isEmail || isPhone) ? TextDecoration.underline : TextDecoration.none, + decoration: (isEmail || isPhone) + ? TextDecoration.underline + : TextDecoration.none, ), ), ); @@ -231,7 +233,7 @@ class _EmployeeDetailPageState extends State { lastName: employee.lastName, size: 45, ), - MySpacing.width(16), + MySpacing.width(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -248,6 +250,34 @@ class _EmployeeDetailPageState extends State { ], ), ), + IconButton( + icon: const Icon(Icons.edit, + size: 24, color: Colors.red), + onPressed: () async { + final result = + await showModalBottomSheet>( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => AddEmployeeBottomSheet( + employeeData: { + 'id': employee.id, + 'first_name': employee.firstName, + 'last_name': employee.lastName, + 'phone_number': employee.phoneNumber, + 'gender': employee.gender.toLowerCase(), + 'job_role_id': employee.jobRoleId, + 'joining_date': + employee.joiningDate?.toIso8601String(), + }, + ), + ); + + if (result != null) { + controller.fetchEmployeeDetails(widget.employeeId); + } + }, + ), ], ), MySpacing.height(14),