added permission based buttons

This commit is contained in:
Vaibhav Surve 2025-08-07 12:26:50 +05:30
parent 93f9a6e738
commit 858fe7435d
3 changed files with 231 additions and 108 deletions

View File

@ -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<String> 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";
}

View File

@ -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<EmployeeDetailPage> {
final EmployeesScreenController controller =
Get.put(EmployeesScreenController());
final PermissionController _permissionController =
Get.find<PermissionController>();
@override
void initState() {
super.initState();
@ -54,90 +57,90 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
/// 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<EmployeeDetailPage> {
);
}),
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<EmployeeDetailPage> {
}),
);
}
}
}

View File

@ -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<EmployeesScreen> with UIMixin {
final EmployeesScreenController _employeeController = Get.put(EmployeesScreenController());
final EmployeesScreenController _employeeController =
Get.put(EmployeesScreenController());
final PermissionController _permissionController =
Get.find<PermissionController>();
final TextEditingController _searchController = TextEditingController();
final RxList<EmployeeModel> _filteredEmployees = <EmployeeModel>[].obs;
@ -31,7 +36,8 @@ class _EmployeesScreenState extends State<EmployeesScreen> 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<EmployeesScreen> 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<EmployeesScreen> 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<EmployeesScreen> 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<ProjectController>(
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<EmployeesScreen> 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<EmployeesScreen> 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<EmployeesScreen> 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<EmployeesScreen> with UIMixin {
}
Widget _buildPopupMenu() {
if (!_permissionController.hasPermission(Permissions.viewAllEmployees)) {
return const SizedBox.shrink();
}
return PopupMenuButton<String>(
icon: Stack(
clipBehavior: Clip.none,
@ -315,7 +341,8 @@ class _EmployeesScreenState extends State<EmployeesScreen> 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<EmployeesScreen> with UIMixin {
activeColor: Colors.red,
side: const BorderSide(color: Colors.black, width: 1.5),
fillColor: MaterialStateProperty.resolveWith<Color>(
(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<EmployeesScreen> 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<EmployeesScreen> 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),
],
),
);