diff --git a/lib/controller/dashboard/daily_task_controller.dart b/lib/controller/dashboard/daily_task_controller.dart index 3dea9f6..f1a3501 100644 --- a/lib/controller/dashboard/daily_task_controller.dart +++ b/lib/controller/dashboard/daily_task_controller.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/model/project_model.dart'; -import 'package:marco/model/dailyTaskPlaning/daily_task_model.dart'; +import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart'; class DailyTaskController extends GetxController { List projects = []; diff --git a/lib/controller/dynamicMenu/dynamic_menu_controller.dart b/lib/controller/dynamicMenu/dynamic_menu_controller.dart new file mode 100644 index 0000000..bbd9738 --- /dev/null +++ b/lib/controller/dynamicMenu/dynamic_menu_controller.dart @@ -0,0 +1,86 @@ +import 'dart:async'; +import 'package:get/get.dart'; +import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/helpers/services/app_logger.dart'; +import 'package:marco/helpers/services/storage/local_storage.dart'; + +class DynamicMenuController extends GetxController { + // UI reactive states + final RxBool isLoading = false.obs; + final RxBool hasError = false.obs; + final RxString errorMessage = ''.obs; + final RxList menuItems = [].obs; + + Timer? _autoRefreshTimer; + @override + void onInit() { + super.onInit(); + fetchMenu(); + + /// Auto refresh every 5 minutes (adjust as needed) + _autoRefreshTimer = Timer.periodic( + const Duration(minutes: 1), + (_) => fetchMenu(), + ); + } + + /// Fetch dynamic menu from API with error and local storage support + Future fetchMenu() async { + isLoading.value = true; + hasError.value = false; + errorMessage.value = ''; + + try { + final responseData = await ApiService.getMenuApi(); + if (responseData != null) { + // Directly parse full JSON into MenuResponse + final menuResponse = MenuResponse.fromJson(responseData); + + menuItems.assignAll(menuResponse.data); + + // Save menus for offline use + await LocalStorage.setMenus(menuItems); + + logSafe("Menu loaded from API with ${menuItems.length} items"); + } else { + // If API fails, load from cache + final cachedMenus = LocalStorage.getMenus(); + if (cachedMenus.isNotEmpty) { + menuItems.assignAll(cachedMenus); + logSafe("Loaded menus from cache: ${menuItems.length} items"); + } else { + hasError.value = true; + errorMessage.value = "Failed to fetch menu"; + menuItems.clear(); + } + } + } catch (e) { + logSafe("Menu fetch exception: $e", level: LogLevel.error); + + // On error, load cached menus + final cachedMenus = LocalStorage.getMenus(); + if (cachedMenus.isNotEmpty) { + menuItems.assignAll(cachedMenus); + logSafe("Loaded menus from cache after error: ${menuItems.length}"); + } else { + hasError.value = true; + errorMessage.value = e.toString(); + menuItems.clear(); + } + } finally { + isLoading.value = false; + } + } + + bool isMenuAllowed(String menuName) { + final menu = menuItems.firstWhereOrNull((m) => m.name == menuName); + return menu?.available ?? false; // default false if not found + } + + @override + void onClose() { + _autoRefreshTimer?.cancel(); // clean up timer + super.onClose(); + } +} diff --git a/lib/controller/task_planing/add_task_controller.dart b/lib/controller/task_planning/add_task_controller.dart similarity index 98% rename from lib/controller/task_planing/add_task_controller.dart rename to lib/controller/task_planning/add_task_controller.dart index 8940fcf..b3df752 100644 --- a/lib/controller/task_planing/add_task_controller.dart +++ b/lib/controller/task_planning/add_task_controller.dart @@ -3,7 +3,7 @@ import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; -import 'package:marco/model/dailyTaskPlaning/master_work_category_model.dart'; +import 'package:marco/model/dailyTaskPlanning/master_work_category_model.dart'; class AddTaskController extends GetxController { RxMap uploadingStates = {}.obs; diff --git a/lib/controller/task_planing/daily_task_planing_controller.dart b/lib/controller/task_planning/daily_task_planning_controller.dart similarity index 97% rename from lib/controller/task_planing/daily_task_planing_controller.dart rename to lib/controller/task_planning/daily_task_planning_controller.dart index b50372f..fc680e2 100644 --- a/lib/controller/task_planing/daily_task_planing_controller.dart +++ b/lib/controller/task_planning/daily_task_planning_controller.dart @@ -4,10 +4,10 @@ import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/model/project_model.dart'; -import 'package:marco/model/dailyTaskPlaning/daily_task_planing_model.dart'; +import 'package:marco/model/dailyTaskPlanning/daily_task_planning_model.dart'; import 'package:marco/model/employees/employee_model.dart'; -class DailyTaskPlaningController extends GetxController { +class DailyTaskPlanningController extends GetxController { List projects = []; List employees = []; List dailyTasks = []; diff --git a/lib/controller/task_planing/report_task_action_controller.dart b/lib/controller/task_planning/report_task_action_controller.dart similarity index 97% rename from lib/controller/task_planing/report_task_action_controller.dart rename to lib/controller/task_planning/report_task_action_controller.dart index 8329898..9722dd0 100644 --- a/lib/controller/task_planing/report_task_action_controller.dart +++ b/lib/controller/task_planning/report_task_action_controller.dart @@ -7,12 +7,12 @@ import 'package:image_picker/image_picker.dart'; import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/controller/my_controller.dart'; -import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; +import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_image_compressor.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; -import 'package:marco/model/dailyTaskPlaning/work_status_model.dart'; +import 'package:marco/model/dailyTaskPlanning/work_status_model.dart'; enum ApiStatus { idle, loading, success, failure } @@ -34,7 +34,7 @@ class ReportTaskActionController extends MyController { final RxString selectedWorkStatusName = ''.obs; final MyFormValidator basicValidator = MyFormValidator(); - final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController()); + final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController()); final ImagePicker _picker = ImagePicker(); final assignedDateController = TextEditingController(); diff --git a/lib/controller/task_planing/report_task_controller.dart b/lib/controller/task_planning/report_task_controller.dart similarity index 97% rename from lib/controller/task_planing/report_task_controller.dart rename to lib/controller/task_planning/report_task_controller.dart index 02e4602..5956b4b 100644 --- a/lib/controller/task_planing/report_task_controller.dart +++ b/lib/controller/task_planning/report_task_controller.dart @@ -6,7 +6,7 @@ import 'package:marco/helpers/services/api_service.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; -import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; +import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; import 'dart:convert'; @@ -14,7 +14,7 @@ import 'package:marco/helpers/widgets/my_image_compressor.dart'; enum ApiStatus { idle, loading, success, failure } -final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController()); +final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController()); final ImagePicker _picker = ImagePicker(); class ReportTaskController extends MyController { diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index ac47286..252ccdc 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,6 +1,6 @@ class ApiEndpoints { - static const String baseUrl = "https://stageapi.marcoaiot.com/api"; - // static const String baseUrl = "https://api.marcoaiot.com/api"; + // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + static const String baseUrl = "https://api.marcoaiot.com/api"; // Dashboard Module API Endpoints static const String getDashboardAttendanceOverview = "/dashboard/attendance-overview"; @@ -62,4 +62,7 @@ class ApiEndpoints { static const String getMasterExpenseTypes = "/master/expenses-types"; static const String updateExpenseStatus = "/expense/action"; static const String deleteExpense = "/expense/delete"; + + ////// Dynamic Menu Module API Endpoints + static const String getDynamicMenu = "/appmenu/get/menu-mobile"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 52e6f46..e1648a4 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -239,6 +239,48 @@ class ApiService { } } +// === Menu APIs === // + + /// Get Sidebar Menu API + static Future?> getMenuApi() async { + logSafe("Fetching sidebar menu..."); + + try { + final response = await _getRequest(ApiEndpoints.getDynamicMenu); + if (response == null) { + logSafe("Menu request failed: null response", level: LogLevel.error); + return null; + } + + final body = response.body.trim(); + if (body.isEmpty) { + logSafe("Menu response body is empty", level: LogLevel.error); + return null; + } + + final jsonResponse = jsonDecode(body); + if (jsonResponse is Map) { + if (jsonResponse['success'] == true) { + logSafe("Sidebar menu fetched successfully"); + return jsonResponse; // ✅ return full response + } else { + logSafe( + "Failed to fetch sidebar menu: ${jsonResponse['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + } + } else { + logSafe("Unexpected response structure: $jsonResponse", + level: LogLevel.error); + } + } catch (e, stack) { + logSafe("Exception during getMenuApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + // === Expense APIs === // /// Edit Expense API @@ -1085,66 +1127,65 @@ class ApiService { ? _parseResponse(res, label: 'Regularization Logs') : null); -static Future uploadAttendanceImage( - String id, - String employeeId, - XFile? imageFile, - double latitude, - double longitude, { - required String imageName, - required String projectId, - String comment = "", - required int action, - bool imageCapture = true, - required String markTime, // 👈 now required - required String date, // 👈 new required param -}) async { - final body = { - "id": id, - "employeeId": employeeId, - "projectId": projectId, - "markTime": markTime, // 👈 directly from UI - "comment": comment, - "action": action, - "date": date, // 👈 directly from UI - if (imageCapture) "latitude": '$latitude', - if (imageCapture) "longitude": '$longitude', - }; + static Future uploadAttendanceImage( + String id, + String employeeId, + XFile? imageFile, + double latitude, + double longitude, { + required String imageName, + required String projectId, + String comment = "", + required int action, + bool imageCapture = true, + required String markTime, // 👈 now required + required String date, // 👈 new required param + }) async { + final body = { + "id": id, + "employeeId": employeeId, + "projectId": projectId, + "markTime": markTime, // 👈 directly from UI + "comment": comment, + "action": action, + "date": date, // 👈 directly from UI + if (imageCapture) "latitude": '$latitude', + if (imageCapture) "longitude": '$longitude', + }; - if (imageCapture && imageFile != null) { - try { - final bytes = await imageFile.readAsBytes(); - final fileSize = await imageFile.length(); - final contentType = "image/${imageFile.path.split('.').last}"; - body["image"] = { - "fileName": imageName, - "contentType": contentType, - "fileSize": fileSize, - "description": "Employee attendance photo", - "base64Data": base64Encode(bytes), - }; - } catch (e) { - logSafe("Image encoding error: $e", level: LogLevel.error); - return false; + if (imageCapture && imageFile != null) { + try { + final bytes = await imageFile.readAsBytes(); + final fileSize = await imageFile.length(); + final contentType = "image/${imageFile.path.split('.').last}"; + body["image"] = { + "fileName": imageName, + "contentType": contentType, + "fileSize": fileSize, + "description": "Employee attendance photo", + "base64Data": base64Encode(bytes), + }; + } catch (e) { + logSafe("Image encoding error: $e", level: LogLevel.error); + return false; + } } + + final response = await _postRequest( + ApiEndpoints.uploadAttendanceImage, + body, + customTimeout: extendedTimeout, + ); + + if (response == null) return false; + + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) return true; + + logSafe("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); + return false; } - final response = await _postRequest( - ApiEndpoints.uploadAttendanceImage, - body, - customTimeout: extendedTimeout, - ); - - if (response == null) return false; - - final json = jsonDecode(response.body); - if (response.statusCode == 200 && json['success'] == true) return true; - - logSafe("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); - return false; -} - - static String generateImageName(String employeeId, int count) { final now = DateTime.now(); final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now); diff --git a/lib/helpers/services/notification_action_handler.dart b/lib/helpers/services/notification_action_handler.dart index 2ac37b8..209b724 100644 --- a/lib/helpers/services/notification_action_handler.dart +++ b/lib/helpers/services/notification_action_handler.dart @@ -3,7 +3,7 @@ import 'package:logger/logger.dart'; import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:marco/controller/dashboard/daily_task_controller.dart'; -import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; +import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:marco/controller/expense/expense_screen_controller.dart'; import 'package:marco/controller/expense/expense_detail_controller.dart'; @@ -88,14 +88,14 @@ class NotificationActionHandler { return; } - _safeControllerUpdate( + _safeControllerUpdate( onFound: (controller) { controller.fetchTaskData(projectId); }, notFoundMessage: - '⚠️ DailyTaskPlaningController not found, cannot refresh.', + '⚠️ DailyTaskPlanningController not found, cannot refresh.', successMessage: - '✅ DailyTaskPlaningController refreshed from notification.', + '✅ DailyTaskPlanningController refreshed from notification.', ); } diff --git a/lib/helpers/services/storage/local_storage.dart b/lib/helpers/services/storage/local_storage.dart index c906a69..90d49f0 100644 --- a/lib/helpers/services/storage/local_storage.dart +++ b/lib/helpers/services/storage/local_storage.dart @@ -7,6 +7,7 @@ import 'package:marco/helpers/services/localizations/language.dart'; import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:marco/model/employees/employee_info.dart'; import 'package:marco/model/user_permission.dart'; +import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart'; class LocalStorage { static const String _loggedInUserKey = "user"; @@ -19,6 +20,7 @@ class LocalStorage { static const String _mpinTokenKey = "mpinToken"; static const String _isMpinKey = "isMpin"; static const String _fcmTokenKey = 'fcm_token'; + static const String _menuStorageKey = "dynamic_menus"; static SharedPreferences? _preferencesInstance; @@ -39,6 +41,30 @@ class LocalStorage { AuthService.isLoggedIn = preferences.getBool(_loggedInUserKey) ?? false; ThemeCustomizer.fromJSON(preferences.getString(_themeCustomizerKey)); } +/// ================== Sidebar Menu ================== +static Future setMenus(List menus) async { + try { + final jsonList = menus.map((e) => e.toJson()).toList(); + return preferences.setString(_menuStorageKey, jsonEncode(jsonList)); + } catch (e) { + print("Error saving menus: $e"); + return false; + } +} + +static List getMenus() { + final storedJson = preferences.getString(_menuStorageKey); + if (storedJson == null) return []; + try { + return (jsonDecode(storedJson) as List) + .map((e) => MenuItem.fromJson(e as Map)) + .toList(); + } catch (e) { + print("Error loading menus: $e"); + return []; + } +} +static Future removeMenus() => preferences.remove(_menuStorageKey); /// ================== User Permissions ================== static Future setUserPermissions( @@ -59,8 +85,8 @@ class LocalStorage { preferences.remove(_userPermissionsKey); /// ================== Employee Info ================== - static Future setEmployeeInfo(EmployeeInfo employeeInfo) => preferences - .setString(_employeeInfoKey, jsonEncode(employeeInfo.toJson())); + static Future setEmployeeInfo(EmployeeInfo employeeInfo) => + preferences.setString(_employeeInfoKey, jsonEncode(employeeInfo.toJson())); static EmployeeInfo? getEmployeeInfo() { final storedJson = preferences.getString(_employeeInfoKey); @@ -92,7 +118,7 @@ class LocalStorage { print("Logout API error: $e"); } - // ===== Local Cleanup ===== + /// ===== Local Cleanup ===== await removeLoggedInUser(); await removeToken(_jwtTokenKey); await removeToken(_refreshTokenKey); @@ -100,6 +126,7 @@ class LocalStorage { await removeEmployeeInfo(); await removeMpinToken(); await removeIsMpin(); + await removeMenus(); // clear menus on logout await preferences.remove("mpin_verified"); await preferences.remove(_languageKey); await preferences.remove(_themeCustomizerKey); diff --git a/lib/model/dailyTaskPlaning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart similarity index 98% rename from lib/model/dailyTaskPlaning/assign_task_bottom_sheet .dart rename to lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index 3eaa40c..b183f8d 100644 --- a/lib/model/dailyTaskPlaning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; +import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; @@ -34,7 +34,7 @@ class AssignTaskBottomSheet extends StatefulWidget { } class _AssignTaskBottomSheetState extends State { - final DailyTaskPlaningController controller = Get.find(); + final DailyTaskPlanningController controller = Get.find(); final ProjectController projectController = Get.find(); final TextEditingController targetController = TextEditingController(); final TextEditingController descriptionController = TextEditingController(); diff --git a/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart b/lib/model/dailyTaskPlanning/comment_task_bottom_sheet.dart similarity index 99% rename from lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart rename to lib/model/dailyTaskPlanning/comment_task_bottom_sheet.dart index 72ae8c0..f3235bc 100644 --- a/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart +++ b/lib/model/dailyTaskPlanning/comment_task_bottom_sheet.dart @@ -4,7 +4,7 @@ import 'package:intl/intl.dart'; import 'dart:io'; import 'dart:math' as math; // --- Assumed Imports (ensure these paths are correct in your project) --- -import 'package:marco/controller/task_planing/report_task_controller.dart'; +import 'package:marco/controller/task_planning/report_task_controller.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_button.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; @@ -13,7 +13,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:marco/helpers/widgets/image_viewer_dialog.dart'; -import 'package:marco/model/dailyTaskPlaning/create_task_botom_sheet.dart'; +import 'package:marco/model/dailyTaskPlanning/create_task_botom_sheet.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; // --- Form Field Keys (Unchanged) --- diff --git a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart b/lib/model/dailyTaskPlanning/create_task_botom_sheet.dart similarity index 99% rename from lib/model/dailyTaskPlaning/create_task_botom_sheet.dart rename to lib/model/dailyTaskPlanning/create_task_botom_sheet.dart index 4c959de..1d5fe62 100644 --- a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart +++ b/lib/model/dailyTaskPlanning/create_task_botom_sheet.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:marco/controller/task_planing/add_task_controller.dart'; +import 'package:marco/controller/task_planning/add_task_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; diff --git a/lib/model/dailyTaskPlaning/daily_progress_report_filter.dart b/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart similarity index 100% rename from lib/model/dailyTaskPlaning/daily_progress_report_filter.dart rename to lib/model/dailyTaskPlanning/daily_progress_report_filter.dart diff --git a/lib/model/dailyTaskPlaning/daily_task_model.dart b/lib/model/dailyTaskPlanning/daily_task_model.dart similarity index 100% rename from lib/model/dailyTaskPlaning/daily_task_model.dart rename to lib/model/dailyTaskPlanning/daily_task_model.dart diff --git a/lib/model/dailyTaskPlaning/daily_task_planing_filter.dart b/lib/model/dailyTaskPlanning/daily_task_planning_filter.dart similarity index 95% rename from lib/model/dailyTaskPlaning/daily_task_planing_filter.dart rename to lib/model/dailyTaskPlanning/daily_task_planning_filter.dart index 8ee8b5a..aa9a521 100644 --- a/lib/model/dailyTaskPlaning/daily_task_planing_filter.dart +++ b/lib/model/dailyTaskPlanning/daily_task_planning_filter.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:marco/controller/permission_controller.dart'; -import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; +import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; -class DailyTaskPlaningFilter extends StatelessWidget { - final DailyTaskPlaningController controller; +class DailyTaskPlanningFilter extends StatelessWidget { + final DailyTaskPlanningController controller; final PermissionController permissionController; - const DailyTaskPlaningFilter({ + const DailyTaskPlanningFilter({ super.key, required this.controller, required this.permissionController, diff --git a/lib/model/dailyTaskPlaning/daily_task_planing_model.dart b/lib/model/dailyTaskPlanning/daily_task_planning_model.dart similarity index 100% rename from lib/model/dailyTaskPlaning/daily_task_planing_model.dart rename to lib/model/dailyTaskPlanning/daily_task_planning_model.dart diff --git a/lib/model/dailyTaskPlaning/master_work_category_model.dart b/lib/model/dailyTaskPlanning/master_work_category_model.dart similarity index 100% rename from lib/model/dailyTaskPlaning/master_work_category_model.dart rename to lib/model/dailyTaskPlanning/master_work_category_model.dart diff --git a/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart b/lib/model/dailyTaskPlanning/report_action_bottom_sheet.dart similarity index 98% rename from lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart rename to lib/model/dailyTaskPlanning/report_action_bottom_sheet.dart index d1002f7..54b333b 100644 --- a/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart +++ b/lib/model/dailyTaskPlanning/report_action_bottom_sheet.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:marco/controller/task_planing/report_task_action_controller.dart'; +import 'package:marco/controller/task_planning/report_task_action_controller.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; @@ -8,8 +8,8 @@ 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:marco/helpers/widgets/image_viewer_dialog.dart'; -import 'package:marco/model/dailyTaskPlaning/create_task_botom_sheet.dart'; -import 'package:marco/model/dailyTaskPlaning/report_action_widgets.dart'; +import 'package:marco/model/dailyTaskPlanning/create_task_botom_sheet.dart'; +import 'package:marco/model/dailyTaskPlanning/report_action_widgets.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; class ReportActionBottomSheet extends StatefulWidget { diff --git a/lib/model/dailyTaskPlaning/report_action_widgets.dart b/lib/model/dailyTaskPlanning/report_action_widgets.dart similarity index 100% rename from lib/model/dailyTaskPlaning/report_action_widgets.dart rename to lib/model/dailyTaskPlanning/report_action_widgets.dart diff --git a/lib/model/dailyTaskPlaning/report_task_bottom_sheet.dart b/lib/model/dailyTaskPlanning/report_task_bottom_sheet.dart similarity index 99% rename from lib/model/dailyTaskPlaning/report_task_bottom_sheet.dart rename to lib/model/dailyTaskPlanning/report_task_bottom_sheet.dart index 68fb793..bd05421 100644 --- a/lib/model/dailyTaskPlaning/report_task_bottom_sheet.dart +++ b/lib/model/dailyTaskPlanning/report_task_bottom_sheet.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:marco/controller/task_planing/report_task_controller.dart'; +import 'package:marco/controller/task_planning/report_task_controller.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_button.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; diff --git a/lib/model/dailyTaskPlaning/task_action_buttons.dart b/lib/model/dailyTaskPlanning/task_action_buttons.dart similarity index 97% rename from lib/model/dailyTaskPlaning/task_action_buttons.dart rename to lib/model/dailyTaskPlanning/task_action_buttons.dart index bf550fd..e15d627 100644 --- a/lib/model/dailyTaskPlaning/task_action_buttons.dart +++ b/lib/model/dailyTaskPlanning/task_action_buttons.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:marco/model/dailyTaskPlaning/comment_task_bottom_sheet.dart'; -import 'package:marco/model/dailyTaskPlaning/report_task_bottom_sheet.dart'; -import 'package:marco/model/dailyTaskPlaning/report_action_bottom_sheet.dart'; +import 'package:marco/model/dailyTaskPlanning/comment_task_bottom_sheet.dart'; +import 'package:marco/model/dailyTaskPlanning/report_task_bottom_sheet.dart'; +import 'package:marco/model/dailyTaskPlanning/report_action_bottom_sheet.dart'; class TaskActionButtons { static Widget reportButton({ diff --git a/lib/model/dailyTaskPlaning/task_list_model.dart b/lib/model/dailyTaskPlanning/task_list_model.dart similarity index 100% rename from lib/model/dailyTaskPlaning/task_list_model.dart rename to lib/model/dailyTaskPlanning/task_list_model.dart diff --git a/lib/model/dailyTaskPlaning/work_status_model.dart b/lib/model/dailyTaskPlanning/work_status_model.dart similarity index 100% rename from lib/model/dailyTaskPlaning/work_status_model.dart rename to lib/model/dailyTaskPlanning/work_status_model.dart diff --git a/lib/model/dynamicMenu/dynamic_menu_model.dart b/lib/model/dynamicMenu/dynamic_menu_model.dart new file mode 100644 index 0000000..8348447 --- /dev/null +++ b/lib/model/dynamicMenu/dynamic_menu_model.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; + +/// Top-level response for the menu API +class MenuResponse { + final bool success; // API call success flag + final String message; // Response message + final List data; // List of menu items + final dynamic errors; // Error details (if any) + final int statusCode; // HTTP-like status code + final DateTime timestamp; // Response timestamp + + MenuResponse({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + /// Creates a MenuResponse from a JSON map + factory MenuResponse.fromJson(Map json) { + return MenuResponse( + success: json['success'] as bool? ?? false, + message: json['message'] as String? ?? '', + data: (json['data'] as List?) + ?.map((e) => MenuItem.fromJson(e as Map)) + .toList() ?? + [], + errors: json['errors'], + statusCode: json['statusCode'] as int? ?? 0, + timestamp: DateTime.tryParse(json['timestamp'] as String? ?? '') ?? + DateTime.now(), + ); + } + + /// Converts the MenuResponse back to JSON map + Map toJson() { + return { + 'success': success, + 'message': message, + 'data': data.map((e) => e.toJson()).toList(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp.toIso8601String(), + }; + } + + /// Parse from raw JSON string + static MenuResponse fromRawJson(String str) => + MenuResponse.fromJson(json.decode(str) as Map); + + /// Convert to raw JSON string + String toRawJson() => json.encode(toJson()); +} + +/// Individual menu item +class MenuItem { + final String id; // Unique item ID + final String name; // Display text + final bool available; // Availability flag + + MenuItem({ + required this.id, + required this.name, + required this.available, + }); + + /// Creates MenuItem from JSON map + factory MenuItem.fromJson(Map json) { + return MenuItem( + id: json['id'] as String? ?? '', + name: json['name'] as String? ?? '', + available: json['available'] as bool? ?? false, + ); + } + + /// Converts MenuItem back to JSON + Map toJson() { + return { + 'id': id, + 'name': name, + 'available': available, + }; + } +} diff --git a/lib/routes.dart b/lib/routes.dart index 4f27766..bda105b 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -10,8 +10,8 @@ import 'package:marco/view/error_pages/error_404_screen.dart'; import 'package:marco/view/error_pages/error_500_screen.dart'; import 'package:marco/view/dashboard/dashboard_screen.dart'; import 'package:marco/view/dashboard/Attendence/attendance_screen.dart'; -import 'package:marco/view/taskPlaning/daily_task_planing.dart'; -import 'package:marco/view/taskPlaning/daily_progress.dart'; +import 'package:marco/view/taskPlanning/daily_task_planning.dart'; +import 'package:marco/view/taskPlanning/daily_progress.dart'; import 'package:marco/view/employees/employees_screen.dart'; import 'package:marco/view/auth/login_option_screen.dart'; import 'package:marco/view/auth/mpin_screen.dart'; @@ -55,8 +55,8 @@ getPageRoute() { middlewares: [AuthMiddleware()]), // Daily Task Planning GetPage( - name: '/dashboard/daily-task-planing', - page: () => DailyTaskPlaningScreen(), + name: '/dashboard/daily-task-Planning', + page: () => DailyTaskPlanningScreen(), middlewares: [AuthMiddleware()]), GetPage( name: '/dashboard/daily-task-progress', diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index e9570fa..915b792 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -11,6 +11,8 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/view/dashboard/dashboard_chart.dart'; import 'package:marco/view/layouts/layout.dart'; +import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart'; + // import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; // ❌ Commented out class DashboardScreen extends StatefulWidget { @@ -21,7 +23,7 @@ class DashboardScreen extends StatefulWidget { static const String projectsRoute = "/dashboard"; static const String attendanceRoute = "/dashboard/attendance"; static const String tasksRoute = "/dashboard/daily-task"; - static const String dailyTasksRoute = "/dashboard/daily-task-planing"; + static const String dailyTasksRoute = "/dashboard/daily-task-Planning"; static const String dailyTasksProgressRoute = "/dashboard/daily-task-progress"; static const String directoryMainPageRoute = "/dashboard/directory-main-page"; @@ -33,7 +35,10 @@ class DashboardScreen extends StatefulWidget { class _DashboardScreenState extends State with UIMixin { final DashboardController dashboardController = - Get.put(DashboardController()); + Get.put(DashboardController(), permanent: true); + final DynamicMenuController menuController = + Get.put(DynamicMenuController(), permanent: true); + bool hasMpin = true; @override @@ -78,77 +83,106 @@ class _DashboardScreenState extends State with UIMixin { ); } - /// Dashboard Statistics Section with ProjectController + /// Dashboard Statistics Section with ProjectController, Obx reactivity for menus Widget _buildDashboardStats(BuildContext context) { - final stats = [ - _StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, - DashboardScreen.attendanceRoute), - _StatItem(LucideIcons.users, "Employees", contentTheme.warning, - DashboardScreen.employeesRoute), - _StatItem(LucideIcons.logs, "Daily Task Planing", contentTheme.info, - DashboardScreen.dailyTasksRoute), - _StatItem(LucideIcons.list_todo, "Daily Task Progress", contentTheme.info, - DashboardScreen.dailyTasksProgressRoute), - _StatItem(LucideIcons.folder, "Directory", contentTheme.info, - DashboardScreen.directoryMainPageRoute), - _StatItem(LucideIcons.badge_dollar_sign, "Expense", contentTheme.info, - DashboardScreen.expenseMainPageRoute), - ]; - - return GetBuilder( - id: 'dashboard_controller', - builder: (controller) { - if (controller.isLoading.value) { - return _buildLoadingSkeleton(context); - } - - final isProjectSelected = controller.selectedProject != null; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isProjectSelected) _buildNoProjectMessage(), - LayoutBuilder( - builder: (context, constraints) { - final maxWidth = constraints.maxWidth; - final crossAxisCount = (maxWidth / 100).floor().clamp(2, 4); - final cardWidth = - (maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount; - - return Wrap( - spacing: 10, - runSpacing: 10, - children: stats - .map((stat) => - _buildStatCard(stat, cardWidth, isProjectSelected)) - .toList(), - ); - }, - ), - ], - ); - }, - ); - } - - /// Attendance Chart Section - Widget _buildAttendanceChartSection() { - return GetBuilder( - id: 'dashboard_controller', - builder: (projectController) { - final isProjectSelected = projectController.selectedProject != null; - return Opacity( - opacity: isProjectSelected ? 1.0 : 0.4, - child: IgnorePointer( - ignoring: !isProjectSelected, - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: AttendanceDashboardChart(), + return Obx(() { + if (menuController.isLoading.value) { + return _buildLoadingSkeleton(context); + } + if (menuController.hasError.value) { + return Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: MyText.bodySmall( + menuController.errorMessage.value, + color: Colors.red, ), ), ); - }, - ); + } + + final stats = [ + _StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, + DashboardScreen.attendanceRoute), + _StatItem(LucideIcons.users, "Employees", contentTheme.warning, + DashboardScreen.employeesRoute), + _StatItem(LucideIcons.logs, "Daily Task Planning", contentTheme.info, + DashboardScreen.dailyTasksRoute), + _StatItem(LucideIcons.list_todo, "Daily Progress Report", + contentTheme.info, DashboardScreen.dailyTasksProgressRoute), + _StatItem(LucideIcons.folder, "Directory", contentTheme.info, + DashboardScreen.directoryMainPageRoute), + _StatItem(LucideIcons.badge_dollar_sign, "Expense", contentTheme.info, + DashboardScreen.expenseMainPageRoute), + ]; + + final projectController = Get.find(); + final isProjectSelected = projectController.selectedProject != null; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isProjectSelected) _buildNoProjectMessage(), + LayoutBuilder( + builder: (context, constraints) { + final maxWidth = constraints.maxWidth; + final crossAxisCount = (maxWidth / 100).floor().clamp(2, 4); + final cardWidth = + (maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount; + + return Wrap( + spacing: 10, + runSpacing: 10, + children: stats + .where((stat) { + final isAllowed = + menuController.isMenuAllowed(stat.title); + + // 👇 Log what is being checked + debugPrint( + "[Dashboard Menu] Checking menu: ${stat.title} -> Allowed: $isAllowed"); + + return isAllowed; + }) + .map((stat) => + _buildStatCard(stat, cardWidth, isProjectSelected)) + .toList(), + ); + }, + ), + ], + ); + }); + } + + /// Attendance Chart Section + /// Attendance Chart Section + Widget _buildAttendanceChartSection() { + return Obx(() { + final isAttendanceAllowed = menuController.isMenuAllowed("Attendance"); + + if (!isAttendanceAllowed) { + // 🚫 Don't render anything if attendance menu is not allowed + return const SizedBox.shrink(); + } + + return GetBuilder( + id: 'dashboard_controller', + builder: (projectController) { + final isProjectSelected = projectController.selectedProject != null; + return Opacity( + opacity: isProjectSelected ? 1.0 : 0.4, + child: IgnorePointer( + ignoring: !isProjectSelected, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: AttendanceDashboardChart(), + ), + ), + ); + }, + ); + }); } /// No Project Assigned Message diff --git a/lib/view/layouts/left_bar.dart b/lib/view/layouts/left_bar.dart index 7fae7e1..3c620d3 100644 --- a/lib/view/layouts/left_bar.dart +++ b/lib/view/layouts/left_bar.dart @@ -124,9 +124,9 @@ class _LeftBarState extends State route: '/dashboard/employees'), NavigationItem( iconData: LucideIcons.logs, - title: "Daily Task Planing", + title: "Daily Task Planning", isCondensed: isCondensed, - route: '/dashboard/daily-task-planing'), + route: '/dashboard/daily-task-Planning'), NavigationItem( iconData: LucideIcons.list_todo, title: "Daily Progress Report", diff --git a/lib/view/taskPlaning/daily_progress.dart b/lib/view/taskPlanning/daily_progress.dart similarity index 99% rename from lib/view/taskPlaning/daily_progress.dart rename to lib/view/taskPlanning/daily_progress.dart index 4ab0c7e..3513281 100644 --- a/lib/view/taskPlaning/daily_progress.dart +++ b/lib/view/taskPlanning/daily_progress.dart @@ -10,10 +10,10 @@ 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/controller/dashboard/daily_task_controller.dart'; -import 'package:marco/model/dailyTaskPlaning/daily_progress_report_filter.dart'; +import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/controller/project_controller.dart'; -import 'package:marco/model/dailyTaskPlaning/task_action_buttons.dart'; +import 'package:marco/model/dailyTaskPlanning/task_action_buttons.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; @@ -92,7 +92,7 @@ class _DailyProgressReportScreenState extends State mainAxisSize: MainAxisSize.min, children: [ MyText.titleLarge( - 'Daily Task Progress', + 'Daily Progress Report', fontWeight: 700, color: Colors.black, ), diff --git a/lib/view/taskPlaning/daily_task_planing.dart b/lib/view/taskPlanning/daily_task_planning.dart similarity index 95% rename from lib/view/taskPlaning/daily_task_planing.dart rename to lib/view/taskPlanning/daily_task_planning.dart index 4525d11..dbf1a6f 100644 --- a/lib/view/taskPlaning/daily_task_planing.dart +++ b/lib/view/taskPlanning/daily_task_planning.dart @@ -6,25 +6,25 @@ 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/controller/task_planing/daily_task_planing_controller.dart'; +import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:percent_indicator/percent_indicator.dart'; -import 'package:marco/model/dailyTaskPlaning/assign_task_bottom_sheet .dart'; +import 'package:marco/model/dailyTaskPlanning/assign_task_bottom_sheet .dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; -class DailyTaskPlaningScreen extends StatefulWidget { - DailyTaskPlaningScreen({super.key}); +class DailyTaskPlanningScreen extends StatefulWidget { + DailyTaskPlanningScreen({super.key}); @override - State createState() => _DailyTaskPlaningScreenState(); + State createState() => _DailyTaskPlanningScreenState(); } -class _DailyTaskPlaningScreenState extends State +class _DailyTaskPlanningScreenState extends State with UIMixin { - final DailyTaskPlaningController dailyTaskPlaningController = - Get.put(DailyTaskPlaningController()); + final DailyTaskPlanningController dailyTaskPlanningController = + Get.put(DailyTaskPlanningController()); final PermissionController permissionController = Get.put(PermissionController()); final ProjectController projectController = Get.find(); @@ -36,7 +36,7 @@ class _DailyTaskPlaningScreenState extends State // Initial fetch if a project is already selected final projectId = projectController.selectedProjectId.value; if (projectId.isNotEmpty) { - dailyTaskPlaningController.fetchTaskData(projectId); + dailyTaskPlanningController.fetchTaskData(projectId); } // Reactive fetch on project ID change @@ -44,7 +44,7 @@ class _DailyTaskPlaningScreenState extends State projectController.selectedProjectId, (newProjectId) { if (newProjectId.isNotEmpty) { - dailyTaskPlaningController.fetchTaskData(newProjectId); + dailyTaskPlanningController.fetchTaskData(newProjectId); } }, ); @@ -77,7 +77,7 @@ class _DailyTaskPlaningScreenState extends State mainAxisSize: MainAxisSize.min, children: [ MyText.titleLarge( - 'Daily Task Planing', + 'Daily Task Planning', fontWeight: 700, color: Colors.black, ), @@ -118,7 +118,7 @@ class _DailyTaskPlaningScreenState extends State final projectId = projectController.selectedProjectId.value; if (projectId.isNotEmpty) { try { - await dailyTaskPlaningController.fetchTaskData(projectId); + await dailyTaskPlanningController.fetchTaskData(projectId); } catch (e) { debugPrint('Error refreshing task data: ${e.toString()}'); } @@ -135,9 +135,9 @@ class _DailyTaskPlaningScreenState extends State kToolbarHeight - MediaQuery.of(context).padding.top, ), - child: GetBuilder( - init: dailyTaskPlaningController, - tag: 'daily_task_planing_controller', + child: GetBuilder( + init: dailyTaskPlanningController, + tag: 'daily_task_Planning_controller', builder: (controller) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -160,8 +160,8 @@ class _DailyTaskPlaningScreenState extends State Widget dailyProgressReportTab() { return Obx(() { - final isLoading = dailyTaskPlaningController.isLoading.value; - final dailyTasks = dailyTaskPlaningController.dailyTasks; + final isLoading = dailyTaskPlanningController.isLoading.value; + final dailyTasks = dailyTaskPlanningController.dailyTasks; if (isLoading) { return SkeletonLoaders.dailyProgressPlanningSkeletonCollapsedOnly();