made chnages into dynamic menus
This commit is contained in:
parent
d229facfba
commit
456df30c8e
@ -25,14 +25,16 @@ class Permissions {
|
|||||||
|
|
||||||
// ------------------- Project Infrastructure --------------------------
|
// ------------------- Project Infrastructure --------------------------
|
||||||
/// Permission to manage project infrastructure (e.g., site details)
|
/// Permission to manage project infrastructure (e.g., site details)
|
||||||
static const String manageProjectInfra = "cf2825ad-453b-46aa-91d9-27c124d63373";
|
static const String manageProjectInfra =
|
||||||
|
"cf2825ad-453b-46aa-91d9-27c124d63373";
|
||||||
|
|
||||||
/// Permission to view infrastructure-related details
|
/// Permission to view infrastructure-related details
|
||||||
static const String viewProjectInfra = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4";
|
static const String viewProjectInfra = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4";
|
||||||
|
|
||||||
// ------------------- Attendance Management ---------------------------
|
// ------------------- Attendance Management ---------------------------
|
||||||
/// Permission to regularize (edit/update) attendance records
|
/// Permission to regularize (edit/update) attendance records
|
||||||
static const String regularizeAttendance = "57802c4a-00aa-4a1f-a048-fd2f70dd44b6";
|
static const String regularizeAttendance =
|
||||||
|
"57802c4a-00aa-4a1f-a048-fd2f70dd44b6";
|
||||||
|
|
||||||
// ------------------- Task Management ---------------------------------
|
// ------------------- Task Management ---------------------------------
|
||||||
/// Permission to create and manage tasks
|
/// Permission to create and manage tasks
|
||||||
@ -90,7 +92,8 @@ class Permissions {
|
|||||||
|
|
||||||
// ------------------- Application Roles -------------------------------
|
// ------------------- Application Roles -------------------------------
|
||||||
/// Application role ID for users with full expense management rights
|
/// Application role ID for users with full expense management rights
|
||||||
static const String expenseManagement = "a4e25142-449b-4334-a6e5-22f70e4732d7";
|
static const String expenseManagement =
|
||||||
|
"a4e25142-449b-4334-a6e5-22f70e4732d7";
|
||||||
|
|
||||||
// ------------------- Document Entities -------------------------------
|
// ------------------- Document Entities -------------------------------
|
||||||
/// Entity ID for project documents
|
/// Entity ID for project documents
|
||||||
@ -118,3 +121,43 @@ class Permissions {
|
|||||||
/// Permission to verify documents
|
/// Permission to verify documents
|
||||||
static const String verifyDocument = "13a1f30f-38d1-41bf-8e7a-b75189aab8e0";
|
static const String verifyDocument = "13a1f30f-38d1-41bf-8e7a-b75189aab8e0";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Contains constants for menu item IDs fetched from the sidebar menu API.
|
||||||
|
class MenuItems {
|
||||||
|
/// Dashboard menu
|
||||||
|
static const String dashboard = "29e03eda-03e8-4714-92fa-67ae0dc53202";
|
||||||
|
|
||||||
|
/// Daily Task Planning menu
|
||||||
|
static const String dailyTaskPlanning =
|
||||||
|
"77ac5205-f823-442e-b9e4-2420d658aa02";
|
||||||
|
|
||||||
|
/// Daily Progress Report menu
|
||||||
|
static const String dailyProgressReport =
|
||||||
|
"299e3cf5-d034-4403-b4a1-ea46d2714832";
|
||||||
|
|
||||||
|
/// Employees menu
|
||||||
|
static const String employees = "78f0206d-c6cc-44d0-832a-2031ed203018";
|
||||||
|
|
||||||
|
/// Attendance menu
|
||||||
|
static const String attendance = "2f212030-f36b-456c-8e7c-11f00f9ba42b";
|
||||||
|
|
||||||
|
/// Directory menu
|
||||||
|
static const String directory = "31bc367b-7c58-4604-95eb-da059a384103";
|
||||||
|
|
||||||
|
/// Expense & Reimbursement menu
|
||||||
|
static const String expenseReimbursement =
|
||||||
|
"0f0dc1a7-1aca-4cdb-9d7a-8a769ce40728";
|
||||||
|
|
||||||
|
/// Payment Requests menu
|
||||||
|
static const String paymentRequests = "b350a59f-2372-4f68-8dcf-f7cfc44523ca";
|
||||||
|
|
||||||
|
/// Advance Payment Statements menu
|
||||||
|
static const String advancePaymentStatements =
|
||||||
|
"e0251cc1-e6d9-417a-9c76-489cc4b6c347";
|
||||||
|
|
||||||
|
/// Finance menu
|
||||||
|
static const String finance = "5ac409dd-bbe0-4d56-bcb9-229bd3a6353c";
|
||||||
|
|
||||||
|
/// Documents menu
|
||||||
|
static const String documents = "92d2cc39-9e6a-46b2-ae50-84fbf83c95d3";
|
||||||
|
}
|
||||||
|
|||||||
@ -33,6 +33,48 @@ class SkeletonLoaders {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inside SkeletonLoaders class
|
||||||
|
static Widget dashboardCardsSkeleton({double? maxWidth}) {
|
||||||
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
|
double width = maxWidth ?? constraints.maxWidth;
|
||||||
|
int crossAxisCount = (width ~/ 80).clamp(2, 4);
|
||||||
|
double cardWidth = (width - (crossAxisCount - 1) * 6) / crossAxisCount;
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: List.generate(6, (index) {
|
||||||
|
return MyCard.bordered(
|
||||||
|
width: cardWidth,
|
||||||
|
height: 60,
|
||||||
|
paddingAll: 4,
|
||||||
|
borderRadiusAll: 5,
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(4),
|
||||||
|
Container(
|
||||||
|
width: cardWidth * 0.5,
|
||||||
|
height: 10,
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Inside SkeletonLoaders class
|
// Inside SkeletonLoaders class
|
||||||
static Widget paymentRequestListSkeletonLoader() {
|
static Widget paymentRequestListSkeletonLoader() {
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
@ -256,6 +298,7 @@ class SkeletonLoaders {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Employee Detail Skeleton Loader
|
// Employee Detail Skeleton Loader
|
||||||
static Widget employeeDetailSkeletonLoader() {
|
static Widget employeeDetailSkeletonLoader() {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
|
|||||||
@ -59,11 +59,13 @@ class MenuItem {
|
|||||||
final String id; // Unique item ID
|
final String id; // Unique item ID
|
||||||
final String name; // Display text
|
final String name; // Display text
|
||||||
final bool available; // Availability flag
|
final bool available; // Availability flag
|
||||||
|
final String mobileLink; // Mobile navigation link
|
||||||
|
|
||||||
MenuItem({
|
MenuItem({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.available,
|
required this.available,
|
||||||
|
required this.mobileLink,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Creates MenuItem from JSON map
|
/// Creates MenuItem from JSON map
|
||||||
@ -72,6 +74,7 @@ class MenuItem {
|
|||||||
id: json['id'] as String? ?? '',
|
id: json['id'] as String? ?? '',
|
||||||
name: json['name'] as String? ?? '',
|
name: json['name'] as String? ?? '',
|
||||||
available: json['available'] as bool? ?? false,
|
available: json['available'] as bool? ?? false,
|
||||||
|
mobileLink: json['mobileLink'] as String? ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +84,7 @@ class MenuItem {
|
|||||||
'id': id,
|
'id': id,
|
||||||
'name': name,
|
'name': name,
|
||||||
'available': available,
|
'available': available,
|
||||||
|
'mobileLink': mobileLink,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,37 +3,24 @@ import 'package:flutter_lucide/flutter_lucide.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||||
import 'package:marco/helpers/widgets/my_card.dart';
|
import 'package:marco/helpers/widgets/my_card.dart';
|
||||||
import 'package:marco/helpers/widgets/my_container.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/helpers/widgets/dashbaord/attendance_overview_chart.dart';
|
import 'package:marco/helpers/widgets/dashbaord/attendance_overview_chart.dart';
|
||||||
import 'package:marco/helpers/widgets/dashbaord/project_progress_chart.dart';
|
import 'package:marco/helpers/widgets/dashbaord/project_progress_chart.dart';
|
||||||
import 'package:marco/view/layouts/layout.dart';
|
|
||||||
import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart';
|
|
||||||
import 'package:marco/helpers/widgets/dashbaord/dashboard_overview_widgets.dart';
|
|
||||||
import 'package:marco/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
|
import 'package:marco/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
|
||||||
import 'package:marco/helpers/widgets/dashbaord/expense_by_status_widget.dart';
|
import 'package:marco/helpers/widgets/dashbaord/expense_by_status_widget.dart';
|
||||||
import 'package:marco/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
|
import 'package:marco/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
|
||||||
|
import 'package:marco/helpers/widgets/dashbaord/dashboard_overview_widgets.dart';
|
||||||
|
import 'package:marco/view/layouts/layout.dart';
|
||||||
|
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
||||||
|
|
||||||
class DashboardScreen extends StatefulWidget {
|
class DashboardScreen extends StatefulWidget {
|
||||||
const DashboardScreen({super.key});
|
const DashboardScreen({super.key});
|
||||||
|
|
||||||
static const String dashboardRoute = "/dashboard";
|
|
||||||
static const String employeesRoute = "/dashboard/employees";
|
|
||||||
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-Planning";
|
|
||||||
static const String dailyTasksProgressRoute =
|
|
||||||
"/dashboard/daily-task-progress";
|
|
||||||
static const String directoryMainPageRoute = "/dashboard/directory-main-page";
|
|
||||||
static const String financeMainPageRoute = "/dashboard/finance";
|
|
||||||
static const String documentMainPageRoute = "/dashboard/document-main-page";
|
|
||||||
static const String serviceprojectsRoute = "/dashboard/service-projects";
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DashboardScreen> createState() => _DashboardScreenState();
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||||
}
|
}
|
||||||
@ -42,6 +29,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
final DashboardController dashboardController =
|
final DashboardController dashboardController =
|
||||||
Get.put(DashboardController(), permanent: true);
|
Get.put(DashboardController(), permanent: true);
|
||||||
final DynamicMenuController menuController = Get.put(DynamicMenuController());
|
final DynamicMenuController menuController = Get.put(DynamicMenuController());
|
||||||
|
final ProjectController projectController = Get.find<ProjectController>();
|
||||||
|
|
||||||
bool hasMpin = true;
|
bool hasMpin = true;
|
||||||
|
|
||||||
@ -60,11 +48,11 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Layout(
|
return Layout(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildDashboardStats(context),
|
_buildDashboardCards(),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
_buildAttendanceChartSection(),
|
_buildAttendanceChartSection(),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
@ -80,13 +68,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
child: DashboardOverviewWidgets.tasksOverview(),
|
child: DashboardOverviewWidgets.tasksOverview(),
|
||||||
),
|
),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
|
|
||||||
ExpenseByStatusWidget(controller: dashboardController),
|
ExpenseByStatusWidget(controller: dashboardController),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
|
|
||||||
// Expense Type Report Chart
|
|
||||||
ExpenseTypeReportChart(),
|
ExpenseTypeReportChart(),
|
||||||
|
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
MonthlyExpenseDashboardChart(),
|
MonthlyExpenseDashboardChart(),
|
||||||
],
|
],
|
||||||
@ -95,7 +79,162 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Project Progress Chart Section
|
/// ---------------- Dynamic Dashboard Cards ----------------
|
||||||
|
Widget _buildDashboardCards() {
|
||||||
|
return Obx(() {
|
||||||
|
if (menuController.isLoading.value) {
|
||||||
|
return SkeletonLoaders.dashboardCardsSkeleton();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuController.hasError.value || menuController.menuItems.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text(
|
||||||
|
"Failed to load menus. Please try again later.",
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final projectSelected = projectController.selectedProject != null;
|
||||||
|
|
||||||
|
// Define dashboard card meta with order
|
||||||
|
final List<String> cardOrder = [
|
||||||
|
MenuItems.attendance,
|
||||||
|
MenuItems.employees,
|
||||||
|
MenuItems.dailyTaskPlanning,
|
||||||
|
MenuItems.dailyProgressReport,
|
||||||
|
MenuItems.directory,
|
||||||
|
MenuItems.finance,
|
||||||
|
MenuItems.documents,
|
||||||
|
];
|
||||||
|
|
||||||
|
final Map<String, _DashboardCardMeta> cardMeta = {
|
||||||
|
MenuItems.attendance:
|
||||||
|
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
|
||||||
|
MenuItems.employees:
|
||||||
|
_DashboardCardMeta(LucideIcons.users, contentTheme.warning),
|
||||||
|
MenuItems.dailyTaskPlanning:
|
||||||
|
_DashboardCardMeta(LucideIcons.logs, contentTheme.info),
|
||||||
|
MenuItems.dailyProgressReport:
|
||||||
|
_DashboardCardMeta(LucideIcons.list_todo, contentTheme.info),
|
||||||
|
MenuItems.directory:
|
||||||
|
_DashboardCardMeta(LucideIcons.folder, contentTheme.info),
|
||||||
|
MenuItems.finance:
|
||||||
|
_DashboardCardMeta(LucideIcons.wallet, contentTheme.info),
|
||||||
|
MenuItems.documents:
|
||||||
|
_DashboardCardMeta(LucideIcons.file_text, contentTheme.info),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter only available menus that exist in cardMeta
|
||||||
|
final allowedMenusMap = {
|
||||||
|
for (var menu in menuController.menuItems)
|
||||||
|
if (menu.available && cardMeta.containsKey(menu.id)) menu.id: menu
|
||||||
|
};
|
||||||
|
|
||||||
|
if (allowedMenusMap.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text(
|
||||||
|
"No accessible modules found.",
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create list of cards in fixed order
|
||||||
|
final stats =
|
||||||
|
cardOrder.where((id) => allowedMenusMap.containsKey(id)).map((id) {
|
||||||
|
final menu = allowedMenusMap[id]!;
|
||||||
|
final meta = cardMeta[id]!;
|
||||||
|
return _DashboardStatItem(
|
||||||
|
meta.icon, menu.name, meta.color, menu.mobileLink);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
|
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4);
|
||||||
|
double cardWidth =
|
||||||
|
(constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
alignment: WrapAlignment.start,
|
||||||
|
children: stats
|
||||||
|
.map((stat) =>
|
||||||
|
_buildDashboardCard(stat, projectSelected, cardWidth))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDashboardCard(
|
||||||
|
_DashboardStatItem stat, bool isProjectSelected, double width) {
|
||||||
|
final isEnabled = stat.title == "Attendance" ? true : isProjectSelected;
|
||||||
|
|
||||||
|
return Opacity(
|
||||||
|
opacity: isEnabled ? 1.0 : 0.4,
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: !isEnabled,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _onDashboardCardTap(stat, isEnabled),
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
child: MyCard.bordered(
|
||||||
|
width: width,
|
||||||
|
height: 60,
|
||||||
|
paddingAll: 4,
|
||||||
|
borderRadiusAll: 5,
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: stat.color.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
stat.icon,
|
||||||
|
size: 16,
|
||||||
|
color: stat.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(4),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
stat.title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDashboardCardTap(_DashboardStatItem statItem, bool isEnabled) {
|
||||||
|
if (!isEnabled) {
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: "No Project Selected",
|
||||||
|
middleText: "Please select a project before accessing this module.",
|
||||||
|
confirm: ElevatedButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: const Text("OK"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Get.toNamed(statItem.route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ---------------- Project Progress Chart ----------------
|
||||||
Widget _buildProjectProgressChartSection() {
|
Widget _buildProjectProgressChartSection() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (dashboardController.projectChartData.isEmpty) {
|
if (dashboardController.projectChartData.isEmpty) {
|
||||||
@ -119,20 +258,16 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attendance Chart Section
|
/// ---------------- Attendance Chart ----------------
|
||||||
Widget _buildAttendanceChartSection() {
|
Widget _buildAttendanceChartSection() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final isAttendanceAllowed = menuController.isMenuAllowed("Attendance");
|
final attendanceMenu = menuController.menuItems
|
||||||
|
.firstWhereOrNull((m) => m.id == MenuItems.attendance);
|
||||||
if (!isAttendanceAllowed) {
|
if (attendanceMenu == null || !attendanceMenu.available)
|
||||||
// 🚫 Don't render anything if attendance menu is not allowed
|
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
|
||||||
|
|
||||||
return GetBuilder<ProjectController>(
|
|
||||||
id: 'dashboard_controller',
|
|
||||||
builder: (projectController) {
|
|
||||||
final isProjectSelected = projectController.selectedProject != null;
|
final isProjectSelected = projectController.selectedProject != null;
|
||||||
|
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: isProjectSelected ? 1.0 : 0.4,
|
opacity: isProjectSelected ? 1.0 : 0.4,
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
@ -146,240 +281,22 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// No Project Assigned Message
|
|
||||||
Widget _buildNoProjectMessage() {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
child: MyCard(
|
|
||||||
color: Colors.orange.withOpacity(0.1),
|
|
||||||
paddingAll: 12,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.info_outline, color: Colors.orange),
|
|
||||||
MySpacing.width(8),
|
|
||||||
Expanded(
|
|
||||||
child: MyText.bodySmall(
|
|
||||||
"No projects assigned yet. Please contact your manager to get started.",
|
|
||||||
color: Colors.orange.shade800,
|
|
||||||
maxLines: 3,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loading Skeletons
|
/// ---------------- Dashboard Card Models ----------------
|
||||||
Widget _buildLoadingSkeleton(BuildContext context) {
|
class _DashboardStatItem {
|
||||||
return Wrap(
|
|
||||||
spacing: 10,
|
|
||||||
runSpacing: 10,
|
|
||||||
children: List.generate(
|
|
||||||
4,
|
|
||||||
(index) =>
|
|
||||||
_buildStatCardSkeleton(MediaQuery.of(context).size.width / 3),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Skeleton Card
|
|
||||||
Widget _buildStatCardSkeleton(double width) {
|
|
||||||
return MyCard.bordered(
|
|
||||||
width: width,
|
|
||||||
height: 100,
|
|
||||||
paddingAll: 5,
|
|
||||||
borderRadiusAll: 5,
|
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
MyContainer.rounded(
|
|
||||||
paddingAll: 12,
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
child: const SizedBox(width: 18, height: 18),
|
|
||||||
),
|
|
||||||
MySpacing.height(8),
|
|
||||||
Container(
|
|
||||||
height: 12,
|
|
||||||
width: 60,
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dashboard Statistics Section
|
|
||||||
Widget _buildDashboardStats(BuildContext context) {
|
|
||||||
return Obx(() {
|
|
||||||
if (menuController.isLoading.value) {
|
|
||||||
return _buildLoadingSkeleton(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (menuController.hasError.value || menuController.menuItems.isEmpty) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Center(
|
|
||||||
child: MyText.bodySmall(
|
|
||||||
"Failed to load menus. Please try again later.",
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final projectController = Get.find<ProjectController>();
|
|
||||||
final isProjectSelected = projectController.selectedProject != null;
|
|
||||||
|
|
||||||
// Keep previous stat items (icons, title, routes)
|
|
||||||
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.wallet, "Finance", contentTheme.info,
|
|
||||||
DashboardScreen.financeMainPageRoute),
|
|
||||||
_StatItem(LucideIcons.file_text, "Documents", contentTheme.info,
|
|
||||||
DashboardScreen.documentMainPageRoute),
|
|
||||||
_StatItem(LucideIcons.briefcase, "Service Projects", contentTheme.info,
|
|
||||||
DashboardScreen.serviceprojectsRoute),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Safe menu check function to avoid exceptions
|
|
||||||
bool _isMenuAllowed(String menuTitle) {
|
|
||||||
try {
|
|
||||||
return menuController.menuItems.isNotEmpty
|
|
||||||
? menuController.isMenuAllowed(menuTitle)
|
|
||||||
: false;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (!isProjectSelected) _buildNoProjectMessage(),
|
|
||||||
LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 8);
|
|
||||||
double cardWidth =
|
|
||||||
(constraints.maxWidth - (crossAxisCount - 1) * 6) /
|
|
||||||
crossAxisCount;
|
|
||||||
|
|
||||||
return Wrap(
|
|
||||||
spacing: 6,
|
|
||||||
runSpacing: 6,
|
|
||||||
alignment: WrapAlignment.start,
|
|
||||||
children: stats
|
|
||||||
.where((stat) =>
|
|
||||||
stat.title == "Service Projects" ||
|
|
||||||
_isMenuAllowed(stat.title))
|
|
||||||
.map((stat) =>
|
|
||||||
_buildStatCard(stat, isProjectSelected, cardWidth))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stat Card (Compact + Small)
|
|
||||||
Widget _buildStatCard(
|
|
||||||
_StatItem statItem, bool isProjectSelected, double width) {
|
|
||||||
const double cardHeight = 60;
|
|
||||||
|
|
||||||
final bool isEnabled = statItem.title == "Attendance" || isProjectSelected;
|
|
||||||
|
|
||||||
return Opacity(
|
|
||||||
opacity: isEnabled ? 1.0 : 0.4,
|
|
||||||
child: IgnorePointer(
|
|
||||||
ignoring: !isEnabled,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => _handleStatCardTap(statItem, isEnabled),
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
child: MyCard.bordered(
|
|
||||||
width: width,
|
|
||||||
height: cardHeight,
|
|
||||||
paddingAll: 4,
|
|
||||||
borderRadiusAll: 5,
|
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_buildStatCardIconCompact(statItem, size: 12),
|
|
||||||
MySpacing.height(4),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
statItem.title,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 8,
|
|
||||||
overflow: TextOverflow.visible,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compact Icon (smaller)
|
|
||||||
Widget _buildStatCardIconCompact(_StatItem statItem, {double size = 12}) {
|
|
||||||
return MyContainer.rounded(
|
|
||||||
paddingAll: 4,
|
|
||||||
color: statItem.color.withOpacity(0.1),
|
|
||||||
child: Icon(
|
|
||||||
statItem.icon,
|
|
||||||
size: size,
|
|
||||||
color: statItem.color,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle Tap
|
|
||||||
void _handleStatCardTap(_StatItem statItem, bool isEnabled) {
|
|
||||||
if (!isEnabled) {
|
|
||||||
Get.defaultDialog(
|
|
||||||
title: "No Project Selected",
|
|
||||||
middleText:
|
|
||||||
"You need to select a project before accessing this section.",
|
|
||||||
confirm: ElevatedButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
child: const Text("OK"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Get.toNamed(statItem.route);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StatItem {
|
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
final Color color;
|
final Color color;
|
||||||
final String route;
|
final String route;
|
||||||
|
|
||||||
_StatItem(this.icon, this.title, this.color, this.route);
|
_DashboardStatItem(this.icon, this.title, this.color, this.route);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DashboardCardMeta {
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
_DashboardCardMeta(this.icon, this.color);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import 'package:marco/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
|
|||||||
import 'package:marco/helpers/widgets/dashbaord/expense_by_status_widget.dart';
|
import 'package:marco/helpers/widgets/dashbaord/expense_by_status_widget.dart';
|
||||||
import 'package:marco/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
|
import 'package:marco/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
|
||||||
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
||||||
|
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||||
|
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
|
||||||
|
|
||||||
class FinanceScreen extends StatefulWidget {
|
class FinanceScreen extends StatefulWidget {
|
||||||
const FinanceScreen({super.key});
|
const FinanceScreen({super.key});
|
||||||
@ -27,6 +29,7 @@ class _FinanceScreenState extends State<FinanceScreen>
|
|||||||
late Animation<double> _fadeAnimation;
|
late Animation<double> _fadeAnimation;
|
||||||
final DashboardController dashboardController =
|
final DashboardController dashboardController =
|
||||||
Get.put(DashboardController(), permanent: true);
|
Get.put(DashboardController(), permanent: true);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -117,8 +120,7 @@ class _FinanceScreenState extends State<FinanceScreen>
|
|||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menuController.hasError.value ||
|
if (menuController.hasError.value || menuController.menuItems.isEmpty) {
|
||||||
menuController.menuItems.isEmpty) {
|
|
||||||
return const Center(
|
return const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Failed to load menus. Please try again later.",
|
"Failed to load menus. Please try again later.",
|
||||||
@ -127,10 +129,18 @@ class _FinanceScreenState extends State<FinanceScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Only allow finance cards if "Expense" menu is allowed
|
// Filter allowed Finance menus dynamically
|
||||||
final isExpenseAllowed = menuController.isMenuAllowed("Expense & Reimbursement");
|
final financeMenuIds = [
|
||||||
|
MenuItems.expenseReimbursement,
|
||||||
|
MenuItems.paymentRequests,
|
||||||
|
MenuItems.advancePaymentStatements,
|
||||||
|
];
|
||||||
|
|
||||||
if (!isExpenseAllowed) {
|
final financeMenus = menuController.menuItems
|
||||||
|
.where((m) => financeMenuIds.contains(m.id) && m.available)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (financeMenus.isEmpty) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"You don’t have access to the Finance section.",
|
"You don’t have access to the Finance section.",
|
||||||
@ -143,7 +153,7 @@ class _FinanceScreenState extends State<FinanceScreen>
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildFinanceModulesCompact(),
|
_buildFinanceModulesCompact(financeMenus),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
ExpenseByStatusWidget(controller: dashboardController),
|
ExpenseByStatusWidget(controller: dashboardController),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
@ -159,30 +169,43 @@ class _FinanceScreenState extends State<FinanceScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Finance Modules (Compact Dashboard-style) ---
|
// --- Finance Modules (Compact Dashboard-style) ---
|
||||||
Widget _buildFinanceModulesCompact() {
|
Widget _buildFinanceModulesCompact(List<MenuItem> financeMenus) {
|
||||||
final stats = [
|
// Map menu IDs to icon + color
|
||||||
_FinanceStatItem(LucideIcons.badge_dollar_sign, "Expense & Reimbursement",
|
final Map<String, _FinanceCardMeta> financeCardMeta = {
|
||||||
contentTheme.info, "/dashboard/expense-main-page"),
|
MenuItems.expenseReimbursement: _FinanceCardMeta(LucideIcons.badge_dollar_sign, contentTheme.info),
|
||||||
_FinanceStatItem(LucideIcons.receipt_text, "Payment Request",
|
MenuItems.paymentRequests: _FinanceCardMeta(LucideIcons.receipt_text, contentTheme.primary),
|
||||||
contentTheme.primary, "/dashboard/payment-request"),
|
MenuItems.advancePaymentStatements: _FinanceCardMeta(LucideIcons.wallet, contentTheme.warning),
|
||||||
_FinanceStatItem(LucideIcons.wallet, "Advance Payment",
|
};
|
||||||
contentTheme.warning, "/dashboard/advance-payment"),
|
|
||||||
];
|
// Build the stat items using API-provided mobileLink
|
||||||
|
final stats = financeMenus.map((menu) {
|
||||||
|
final meta = financeCardMeta[menu.id]!;
|
||||||
|
|
||||||
|
// --- Log the routing info ---
|
||||||
|
debugPrint(
|
||||||
|
"[Finance Card] ID: ${menu.id}, Title: ${menu.name}, Route: ${menu.mobileLink}");
|
||||||
|
|
||||||
|
return _FinanceStatItem(
|
||||||
|
meta.icon,
|
||||||
|
menu.name,
|
||||||
|
meta.color,
|
||||||
|
menu.mobileLink, // Each card navigates to its own route
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
final projectSelected = projectController.selectedProject != null;
|
final projectSelected = projectController.selectedProject != null;
|
||||||
|
|
||||||
return LayoutBuilder(builder: (context, constraints) {
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
|
// Determine number of columns dynamically
|
||||||
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4);
|
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4);
|
||||||
double cardWidth =
|
double cardWidth = (constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
|
||||||
(constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
|
|
||||||
|
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 6,
|
spacing: 6,
|
||||||
runSpacing: 6,
|
runSpacing: 6,
|
||||||
alignment: WrapAlignment.end,
|
alignment: WrapAlignment.end,
|
||||||
children: stats
|
children: stats
|
||||||
.map((stat) =>
|
.map((stat) => _buildFinanceModuleCard(stat, projectSelected, cardWidth))
|
||||||
_buildFinanceModuleCard(stat, projectSelected, cardWidth))
|
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -190,14 +213,12 @@ class _FinanceScreenState extends State<FinanceScreen>
|
|||||||
|
|
||||||
Widget _buildFinanceModuleCard(
|
Widget _buildFinanceModuleCard(
|
||||||
_FinanceStatItem stat, bool isProjectSelected, double width) {
|
_FinanceStatItem stat, bool isProjectSelected, double width) {
|
||||||
final bool isEnabled = isProjectSelected;
|
|
||||||
|
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: isEnabled ? 1.0 : 0.4,
|
opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
ignoring: !isEnabled,
|
ignoring: !isProjectSelected,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _onCardTap(stat, isEnabled),
|
onTap: () => _onCardTap(stat, isProjectSelected),
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
child: MyCard.bordered(
|
child: MyCard.bordered(
|
||||||
width: width,
|
width: width,
|
||||||
@ -252,6 +273,7 @@ class _FinanceScreenState extends State<FinanceScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// Navigate to the card's specific route
|
||||||
Get.toNamed(statItem.route);
|
Get.toNamed(statItem.route);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -265,3 +287,9 @@ class _FinanceStatItem {
|
|||||||
|
|
||||||
_FinanceStatItem(this.icon, this.title, this.color, this.route);
|
_FinanceStatItem(this.icon, this.title, this.color, this.route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _FinanceCardMeta {
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
_FinanceCardMeta(this.icon, this.color);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user