From 858fe7435d6cdaedc335e4d4d18551d56372c1a8 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 7 Aug 2025 12:26:50 +0530 Subject: [PATCH] added permission based buttons --- lib/helpers/utils/permission_constants.dart | 81 ++++++++- .../employees/employee_detail_screen.dart | 164 +++++++++--------- lib/view/employees/employees_screen.dart | 94 +++++++--- 3 files changed, 231 insertions(+), 108 deletions(-) diff --git a/lib/helpers/utils/permission_constants.dart b/lib/helpers/utils/permission_constants.dart index 81ee465..c576f37 100644 --- a/lib/helpers/utils/permission_constants.dart +++ b/lib/helpers/utils/permission_constants.dart @@ -1,25 +1,94 @@ +/// Contains all role and permission UUIDs used for access control across the application. class Permissions { + // ------------------- Project Management ------------------------------ + /// Permission to manage master data (like dropdowns, configurations) static const String manageMaster = "588a8824-f924-4955-82d8-fc51956cf323"; + + /// Permission to create, edit, delete projects static const String manageProject = "172fc9b6-755b-4f62-ab26-55c34a330614"; + + /// Permission to view list of all projects static const String viewProjects = "6ea44136-987e-44ba-9e5d-1cf8f5837ebc"; + + /// Permission to assign employees to a project + static const String assignToProject = "b94802ce-0689-4643-9e1d-11c86950c35b"; + + // ------------------- Employee Management ----------------------------- + /// Permission to manage employee records static const String manageEmployees = "a97d366a-c2bb-448d-be93-402bd2324566"; - static const String manageProjectInfra = "f2aee20a-b754-4537-8166-f9507b44585b"; - static const String viewProjectInfra = "c7b68e33-72f0-474f-bd96-77636427ecc8"; + + /// Permission to view all employees + static const String viewAllEmployees = "60611762-7f8a-4fb5-b53f-b1139918796b"; + + /// Permission to view only team members (subordinate employees) + static const String viewTeamMembers = "b82d2b7e-0d52-45f3-997b-c008ea460e7f"; + + // ------------------- Project Infrastructure -------------------------- + /// Permission to manage project infrastructure (e.g., site details) + static const String manageProjectInfra = "cf2825ad-453b-46aa-91d9-27c124d63373"; + + /// Permission to view infrastructure-related details + static const String viewProjectInfra = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"; + + // ------------------- Attendance Management --------------------------- + /// Permission to regularize (edit/update) attendance records static const String regularizeAttendance = "57802c4a-00aa-4a1f-a048-fd2f70dd44b6"; - static const String assignToProject = "fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"; - static const String infrastructure = "9666de86-d7c7-4d3d-acaa-fcd6d6b81f3c"; + + // ------------------- Task Management --------------------------------- + /// Permission to create and manage tasks static const String manageTask = "08752f33-3b29-4816-b76b-ea8a968ed3c5"; + + /// Permission to approve tasks + static const String approveTask = "db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"; + + /// Permission to view task lists and details static const String viewTask = "9fcc5f87-25e3-4846-90ac-67a71ab92e3c"; + + /// Permission to assign tasks for reporting static const String assignReportTask = "6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"; + + // ------------------- Directory Roles --------------------------------- + /// Admin-level directory access static const String directoryAdmin = "4286a13b-bb40-4879-8c6d-18e9e393beda"; + + /// Manager-level directory access static const String directoryManager = "62668630-13ce-4f52-a0f0-db38af2230c5"; - // Expense Permissions + /// Basic directory user access + static const String directoryUser = "0f919170-92d4-4337-abd3-49b66fc871bb"; + + // ------------------- Expense Permissions ----------------------------- + /// View only own expenses static const String expenseViewSelf = "385be49f-8fde-440e-bdbc-3dffeb8dd116"; + + /// View all employee expenses static const String expenseViewAll = "01e06444-9ca7-4df4-b900-8c3fa051b92f"; + + /// Create/upload new expenses static const String expenseUpload = "0f57885d-bcb2-4711-ac95-d841ace6d5a7"; + + /// Review submitted expenses static const String expenseReview = "1f4bda08-1873-449a-bb66-3e8222bd871b"; + + /// Approve or reject expenses static const String expenseApprove = "eaafdd76-8aac-45f9-a530-315589c6deca"; + + /// Process expenses for payment or final action static const String expenseProcess = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"; - static const String expenseManage = "bdee29a2-b73b-402d-8dd1-c4b1f81ccbc3"; + + /// Full access to manage all expense operations + static const String expenseManage = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"; + + /// ID used to track expenses in "Draft" status + static const String expenseDraft = "297e0d8f-f668-41b5-bfea-e03b354251c8"; + + /// List of user IDs who rejected the expense (used for audit trail) + static const List expenseRejectedBy = [ + "d1ee5eec-24b6-4364-8673-a8f859c60729", + "965eda62-7907-4963-b4a1-657fb0b2724b", + ]; + + // ------------------- Application Roles ------------------------------- + /// Application role ID for users with full expense management rights + static const String expenseManagement = "a4e25142-449b-4334-a6e5-22f70e4732d7"; } diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 29558af..ceee173 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -8,6 +8,8 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; import 'package:marco/helpers/utils/launcher_utils.dart'; +import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/helpers/utils/permission_constants.dart'; class EmployeeDetailPage extends StatefulWidget { final String employeeId; @@ -21,7 +23,8 @@ class EmployeeDetailPage extends StatefulWidget { class _EmployeeDetailPageState extends State { final EmployeesScreenController controller = Get.put(EmployeesScreenController()); - + final PermissionController _permissionController = + Get.find(); @override void initState() { super.initState(); @@ -54,90 +57,90 @@ class _EmployeeDetailPageState extends State { /// Row builder with email/phone tap & copy support Widget _buildLabelValueRow(String label, String value, - {bool isMultiLine = false}) { - final lowerLabel = label.toLowerCase(); - final isEmail = lowerLabel == 'email'; - final isPhone = lowerLabel == 'phone number' || - lowerLabel == 'emergency phone number'; + {bool isMultiLine = false}) { + final lowerLabel = label.toLowerCase(); + final isEmail = lowerLabel == 'email'; + final isPhone = + lowerLabel == 'phone number' || lowerLabel == 'emergency phone number'; - void handleTap() { - if (value == 'NA') return; - if (isEmail) { - LauncherUtils.launchEmail(value); - } else if (isPhone) { - LauncherUtils.launchPhone(value); + void handleTap() { + if (value == 'NA') return; + if (isEmail) { + LauncherUtils.launchEmail(value); + } else if (isPhone) { + LauncherUtils.launchPhone(value); + } } - } - void handleLongPress() { - if (value == 'NA') return; - LauncherUtils.copyToClipboard(value, typeLabel: label); - } + void handleLongPress() { + if (value == 'NA') return; + LauncherUtils.copyToClipboard(value, typeLabel: label); + } - final valueWidget = GestureDetector( - onTap: (isEmail || isPhone) ? handleTap : null, - onLongPress: (isEmail || isPhone) ? handleLongPress : null, - child: Text( - value, - style: TextStyle( - fontWeight: FontWeight.normal, - color: (isEmail || isPhone) ? Colors.indigo : Colors.black54, - fontSize: 14, - decoration: - (isEmail || isPhone) ? TextDecoration.underline : TextDecoration.none, - ), - ), - ); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isMultiLine) ...[ - Text( - label, - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black87, - fontSize: 14, - ), + final valueWidget = GestureDetector( + onTap: (isEmail || isPhone) ? handleTap : null, + onLongPress: (isEmail || isPhone) ? handleLongPress : null, + child: Text( + value, + style: TextStyle( + fontWeight: FontWeight.normal, + color: (isEmail || isPhone) ? Colors.indigo : Colors.black54, + fontSize: 14, + decoration: (isEmail || isPhone) + ? TextDecoration.underline + : TextDecoration.none, ), - MySpacing.height(4), - valueWidget, - ] else - GestureDetector( - onTap: (isEmail || isPhone) ? handleTap : null, - onLongPress: (isEmail || isPhone) ? handleLongPress : null, - child: RichText( - text: TextSpan( - text: "$label: ", - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black87, - fontSize: 14, - ), - children: [ - TextSpan( - text: value, - style: TextStyle( - fontWeight: FontWeight.normal, - color: - (isEmail || isPhone) ? Colors.indigo : Colors.black54, - decoration: (isEmail || isPhone) - ? TextDecoration.underline - : TextDecoration.none, - ), - ), - ], + ), + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isMultiLine) ...[ + Text( + label, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black87, + fontSize: 14, ), ), - ), - MySpacing.height(10), - Divider(color: Colors.grey[300], height: 1), - MySpacing.height(10), - ], - ); -} - + MySpacing.height(4), + valueWidget, + ] else + GestureDetector( + onTap: (isEmail || isPhone) ? handleTap : null, + onLongPress: (isEmail || isPhone) ? handleLongPress : null, + child: RichText( + text: TextSpan( + text: "$label: ", + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black87, + fontSize: 14, + ), + children: [ + TextSpan( + text: value, + style: TextStyle( + fontWeight: FontWeight.normal, + color: + (isEmail || isPhone) ? Colors.indigo : Colors.black54, + decoration: (isEmail || isPhone) + ? TextDecoration.underline + : TextDecoration.none, + ), + ), + ], + ), + ), + ), + MySpacing.height(10), + Divider(color: Colors.grey[300], height: 1), + MySpacing.height(10), + ], + ); + } /// Info card Widget _buildInfoCard(employee) { @@ -291,6 +294,9 @@ class _EmployeeDetailPageState extends State { ); }), floatingActionButton: Obx(() { + if (!_permissionController.hasPermission(Permissions.assignToProject)) { + return const SizedBox.shrink(); + } if (controller.isLoadingEmployeeDetails.value || controller.selectedEmployeeDetails.value == null) { return const SizedBox.shrink(); @@ -318,4 +324,4 @@ class _EmployeeDetailPageState extends State { }), ); } -} \ No newline at end of file +} diff --git a/lib/view/employees/employees_screen.dart b/lib/view/employees/employees_screen.dart index 813e3a8..e7c3a10 100644 --- a/lib/view/employees/employees_screen.dart +++ b/lib/view/employees/employees_screen.dart @@ -13,6 +13,8 @@ import 'package:marco/view/employees/employee_detail_screen.dart'; import 'package:marco/model/employee_model.dart'; import 'package:marco/helpers/utils/launcher_utils.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; +import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/helpers/utils/permission_constants.dart'; class EmployeesScreen extends StatefulWidget { const EmployeesScreen({super.key}); @@ -22,7 +24,10 @@ class EmployeesScreen extends StatefulWidget { } class _EmployeesScreenState extends State with UIMixin { - final EmployeesScreenController _employeeController = Get.put(EmployeesScreenController()); + final EmployeesScreenController _employeeController = + Get.put(EmployeesScreenController()); + final PermissionController _permissionController = + Get.find(); final TextEditingController _searchController = TextEditingController(); final RxList _filteredEmployees = [].obs; @@ -31,7 +36,8 @@ class _EmployeesScreenState extends State with UIMixin { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _initEmployees(); - _searchController.addListener(() => _filterEmployees(_searchController.text)); + _searchController + .addListener(() => _filterEmployees(_searchController.text)); }); } @@ -84,11 +90,12 @@ class _EmployeesScreenState extends State with UIMixin { final q = query.toLowerCase(); _filteredEmployees.assignAll( - employees.where((e) => - e.name.toLowerCase().contains(q) || - e.email.toLowerCase().contains(q) || - e.phoneNumber.toLowerCase().contains(q) || - e.jobRole.toLowerCase().contains(q), + employees.where( + (e) => + e.name.toLowerCase().contains(q) || + e.email.toLowerCase().contains(q) || + e.phoneNumber.toLowerCase().contains(q) || + e.jobRole.toLowerCase().contains(q), ), ); } @@ -172,7 +179,8 @@ class _EmployeesScreenState extends State with UIMixin { child: Row( children: [ IconButton( - icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20), + icon: const Icon(Icons.arrow_back_ios_new, + color: Colors.black, size: 20), onPressed: () => Get.offNamed('/dashboard'), ), MySpacing.width(8), @@ -180,14 +188,18 @@ class _EmployeesScreenState extends State with UIMixin { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.titleLarge('Employees', fontWeight: 700, color: Colors.black), + MyText.titleLarge('Employees', + fontWeight: 700, color: Colors.black), MySpacing.height(2), GetBuilder( builder: (projectController) { - final projectName = projectController.selectedProject?.name ?? 'Select Project'; + final projectName = + projectController.selectedProject?.name ?? + 'Select Project'; return Row( children: [ - const Icon(Icons.work_outline, size: 14, color: Colors.grey), + const Icon(Icons.work_outline, + size: 14, color: Colors.grey), MySpacing.width(4), Expanded( child: MyText.bodySmall( @@ -212,6 +224,11 @@ class _EmployeesScreenState extends State with UIMixin { } Widget _buildFloatingActionButton() { + if (!_permissionController.hasPermission(Permissions.manageEmployees)) { + return const SizedBox + .shrink(); + } + return InkWell( onTap: _onAddNewEmployee, borderRadius: BorderRadius.circular(28), @@ -220,7 +237,10 @@ class _EmployeesScreenState extends State with UIMixin { decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(28), - boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 3))], + boxShadow: const [ + BoxShadow( + color: Colors.black26, blurRadius: 6, offset: Offset(0, 3)) + ], ), child: const Row( mainAxisSize: MainAxisSize.min, @@ -257,9 +277,11 @@ class _EmployeesScreenState extends State with UIMixin { style: const TextStyle(fontSize: 13, height: 1.2), decoration: InputDecoration( isDense: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + contentPadding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 8), prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), - prefixIconConstraints: const BoxConstraints(minWidth: 32, minHeight: 32), + prefixIconConstraints: + const BoxConstraints(minWidth: 32, minHeight: 32), hintText: 'Search contacts...', hintStyle: const TextStyle(fontSize: 13, color: Colors.grey), filled: true, @@ -303,6 +325,10 @@ class _EmployeesScreenState extends State with UIMixin { } Widget _buildPopupMenu() { + if (!_permissionController.hasPermission(Permissions.viewAllEmployees)) { + return const SizedBox.shrink(); + } + return PopupMenuButton( icon: Stack( clipBehavior: Clip.none, @@ -315,7 +341,8 @@ class _EmployeesScreenState extends State with UIMixin { child: Container( width: 10, height: 10, - decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle), + decoration: const BoxDecoration( + color: Colors.red, shape: BoxShape.circle), ), ) : const SizedBox.shrink()), @@ -341,7 +368,9 @@ class _EmployeesScreenState extends State with UIMixin { activeColor: Colors.red, side: const BorderSide(color: Colors.black, width: 1.5), fillColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) ? Colors.red : Colors.white), + (states) => states.contains(MaterialState.selected) + ? Colors.red + : Colors.white), ), const Text('All Employees'), ], @@ -370,7 +399,8 @@ class _EmployeesScreenState extends State with UIMixin { return Padding( padding: const EdgeInsets.only(top: 60), child: Center( - child: MyText.bodySmall("No Employees Found", fontWeight: 600, color: Colors.grey[700]), + child: MyText.bodySmall("No Employees Found", + fontWeight: 600, color: Colors.grey[700]), ), ); } @@ -398,19 +428,37 @@ class _EmployeesScreenState extends State with UIMixin { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.titleSmall(e.name, fontWeight: 600, overflow: TextOverflow.ellipsis), + MyText.titleSmall(e.name, + fontWeight: 600, overflow: TextOverflow.ellipsis), if (e.jobRole.isNotEmpty) - MyText.bodySmall(e.jobRole, color: Colors.grey[700], overflow: TextOverflow.ellipsis), + MyText.bodySmall(e.jobRole, + color: Colors.grey[700], + overflow: TextOverflow.ellipsis), MySpacing.height(8), if (e.email.isNotEmpty && e.email != '-') - _buildLinkRow(icon: Icons.email_outlined, text: e.email, onTap: () => LauncherUtils.launchEmail(e.email), onLongPress: () => LauncherUtils.copyToClipboard(e.email, typeLabel: 'Email')), - if (e.email.isNotEmpty && e.email != '-') MySpacing.height(6), + _buildLinkRow( + icon: Icons.email_outlined, + text: e.email, + onTap: () => LauncherUtils.launchEmail(e.email), + onLongPress: () => LauncherUtils.copyToClipboard( + e.email, + typeLabel: 'Email')), + if (e.email.isNotEmpty && e.email != '-') + MySpacing.height(6), if (e.phoneNumber.isNotEmpty) - _buildLinkRow(icon: Icons.phone_outlined, text: e.phoneNumber, onTap: () => LauncherUtils.launchPhone(e.phoneNumber), onLongPress: () => LauncherUtils.copyToClipboard(e.phoneNumber, typeLabel: 'Phone')), + _buildLinkRow( + icon: Icons.phone_outlined, + text: e.phoneNumber, + onTap: () => + LauncherUtils.launchPhone(e.phoneNumber), + onLongPress: () => LauncherUtils.copyToClipboard( + e.phoneNumber, + typeLabel: 'Phone')), ], ), ), - const Icon(Icons.arrow_forward_ios, color: Colors.grey, size: 16), + const Icon(Icons.arrow_forward_ios, + color: Colors.grey, size: 16), ], ), );