made chnages into dynamic menus
This commit is contained in:
parent
d229facfba
commit
456df30c8e
@ -25,14 +25,16 @@ class Permissions {
|
||||
|
||||
// ------------------- Project Infrastructure --------------------------
|
||||
/// 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
|
||||
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 regularizeAttendance =
|
||||
"57802c4a-00aa-4a1f-a048-fd2f70dd44b6";
|
||||
|
||||
// ------------------- Task Management ---------------------------------
|
||||
/// Permission to create and manage tasks
|
||||
@ -90,7 +92,8 @@ class Permissions {
|
||||
|
||||
// ------------------- Application Roles -------------------------------
|
||||
/// 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 -------------------------------
|
||||
/// Entity ID for project documents
|
||||
@ -118,3 +121,43 @@ class Permissions {
|
||||
/// Permission to verify documents
|
||||
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
|
||||
static Widget paymentRequestListSkeletonLoader() {
|
||||
return ListView.separated(
|
||||
@ -256,6 +298,7 @@ class SkeletonLoaders {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Employee Detail Skeleton Loader
|
||||
static Widget employeeDetailSkeletonLoader() {
|
||||
return SingleChildScrollView(
|
||||
|
||||
@ -59,11 +59,13 @@ class MenuItem {
|
||||
final String id; // Unique item ID
|
||||
final String name; // Display text
|
||||
final bool available; // Availability flag
|
||||
final String mobileLink; // Mobile navigation link
|
||||
|
||||
MenuItem({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.available,
|
||||
required this.mobileLink,
|
||||
});
|
||||
|
||||
/// Creates MenuItem from JSON map
|
||||
@ -72,6 +74,7 @@ class MenuItem {
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
available: json['available'] as bool? ?? false,
|
||||
mobileLink: json['mobileLink'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@ -81,6 +84,7 @@ class MenuItem {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'available': available,
|
||||
'mobileLink': mobileLink,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,37 +3,24 @@ import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/dashboard/dashboard_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/utils/mixins/ui_mixin.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_text.dart';
|
||||
import 'package:marco/helpers/widgets/dashbaord/attendance_overview_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_by_status_widget.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 {
|
||||
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
|
||||
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||
}
|
||||
@ -42,6 +29,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
final DashboardController dashboardController =
|
||||
Get.put(DashboardController(), permanent: true);
|
||||
final DynamicMenuController menuController = Get.put(DynamicMenuController());
|
||||
final ProjectController projectController = Get.find<ProjectController>();
|
||||
|
||||
bool hasMpin = true;
|
||||
|
||||
@ -60,11 +48,11 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(10),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDashboardStats(context),
|
||||
_buildDashboardCards(),
|
||||
MySpacing.height(24),
|
||||
_buildAttendanceChartSection(),
|
||||
MySpacing.height(24),
|
||||
@ -80,13 +68,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
child: DashboardOverviewWidgets.tasksOverview(),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
|
||||
ExpenseByStatusWidget(controller: dashboardController),
|
||||
MySpacing.height(24),
|
||||
|
||||
// Expense Type Report Chart
|
||||
ExpenseTypeReportChart(),
|
||||
|
||||
MySpacing.height(24),
|
||||
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() {
|
||||
return Obx(() {
|
||||
if (dashboardController.projectChartData.isEmpty) {
|
||||
@ -119,267 +258,45 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
});
|
||||
}
|
||||
|
||||
/// Attendance Chart Section
|
||||
/// ---------------- Attendance Chart ----------------
|
||||
Widget _buildAttendanceChartSection() {
|
||||
return Obx(() {
|
||||
final isAttendanceAllowed = menuController.isMenuAllowed("Attendance");
|
||||
|
||||
if (!isAttendanceAllowed) {
|
||||
// 🚫 Don't render anything if attendance menu is not allowed
|
||||
final attendanceMenu = menuController.menuItems
|
||||
.firstWhereOrNull((m) => m.id == MenuItems.attendance);
|
||||
if (attendanceMenu == null || !attendanceMenu.available)
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return GetBuilder<ProjectController>(
|
||||
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(5),
|
||||
child: SizedBox(
|
||||
height: 400,
|
||||
child: AttendanceDashboardChart(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 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
|
||||
Widget _buildLoadingSkeleton(BuildContext context) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
return Opacity(
|
||||
opacity: isProjectSelected ? 1.0 : 0.4,
|
||||
child: IgnorePointer(
|
||||
ignoring: !isProjectSelected,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: SizedBox(
|
||||
height: 400,
|
||||
child: AttendanceDashboardChart(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// ---------------- Dashboard Card Models ----------------
|
||||
class _DashboardStatItem {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final Color color;
|
||||
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/monthly_expense_dashboard_chart.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 {
|
||||
const FinanceScreen({super.key});
|
||||
@ -27,6 +29,7 @@ class _FinanceScreenState extends State<FinanceScreen>
|
||||
late Animation<double> _fadeAnimation;
|
||||
final DashboardController dashboardController =
|
||||
Get.put(DashboardController(), permanent: true);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -117,8 +120,7 @@ class _FinanceScreenState extends State<FinanceScreen>
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (menuController.hasError.value ||
|
||||
menuController.menuItems.isEmpty) {
|
||||
if (menuController.hasError.value || menuController.menuItems.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"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
|
||||
final isExpenseAllowed = menuController.isMenuAllowed("Expense & Reimbursement");
|
||||
// Filter allowed Finance menus dynamically
|
||||
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(
|
||||
child: Text(
|
||||
"You don’t have access to the Finance section.",
|
||||
@ -143,7 +153,7 @@ class _FinanceScreenState extends State<FinanceScreen>
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildFinanceModulesCompact(),
|
||||
_buildFinanceModulesCompact(financeMenus),
|
||||
MySpacing.height(24),
|
||||
ExpenseByStatusWidget(controller: dashboardController),
|
||||
MySpacing.height(24),
|
||||
@ -159,103 +169,115 @@ class _FinanceScreenState extends State<FinanceScreen>
|
||||
}
|
||||
|
||||
// --- Finance Modules (Compact Dashboard-style) ---
|
||||
Widget _buildFinanceModulesCompact() {
|
||||
final stats = [
|
||||
_FinanceStatItem(LucideIcons.badge_dollar_sign, "Expense & Reimbursement",
|
||||
contentTheme.info, "/dashboard/expense-main-page"),
|
||||
_FinanceStatItem(LucideIcons.receipt_text, "Payment Request",
|
||||
contentTheme.primary, "/dashboard/payment-request"),
|
||||
_FinanceStatItem(LucideIcons.wallet, "Advance Payment",
|
||||
contentTheme.warning, "/dashboard/advance-payment"),
|
||||
];
|
||||
Widget _buildFinanceModulesCompact(List<MenuItem> financeMenus) {
|
||||
// Map menu IDs to icon + color
|
||||
final Map<String, _FinanceCardMeta> financeCardMeta = {
|
||||
MenuItems.expenseReimbursement: _FinanceCardMeta(LucideIcons.badge_dollar_sign, contentTheme.info),
|
||||
MenuItems.paymentRequests: _FinanceCardMeta(LucideIcons.receipt_text, contentTheme.primary),
|
||||
MenuItems.advancePaymentStatements: _FinanceCardMeta(LucideIcons.wallet, contentTheme.warning),
|
||||
};
|
||||
|
||||
final projectSelected = projectController.selectedProject != null;
|
||||
// Build the stat items using API-provided mobileLink
|
||||
final stats = financeMenus.map((menu) {
|
||||
final meta = financeCardMeta[menu.id]!;
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4);
|
||||
double cardWidth =
|
||||
(constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
|
||||
// --- Log the routing info ---
|
||||
debugPrint(
|
||||
"[Finance Card] ID: ${menu.id}, Title: ${menu.name}, Route: ${menu.mobileLink}");
|
||||
|
||||
return Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
alignment: WrapAlignment.end,
|
||||
children: stats
|
||||
.map((stat) =>
|
||||
_buildFinanceModuleCard(stat, projectSelected, cardWidth))
|
||||
.toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
return _FinanceStatItem(
|
||||
meta.icon,
|
||||
menu.name,
|
||||
meta.color,
|
||||
menu.mobileLink, // Each card navigates to its own route
|
||||
);
|
||||
}).toList();
|
||||
|
||||
Widget _buildFinanceModuleCard(
|
||||
_FinanceStatItem stat, bool isProjectSelected, double width) {
|
||||
final bool isEnabled = isProjectSelected;
|
||||
final projectSelected = projectController.selectedProject != null;
|
||||
|
||||
return Opacity(
|
||||
opacity: isEnabled ? 1.0 : 0.4,
|
||||
child: IgnorePointer(
|
||||
ignoring: !isEnabled,
|
||||
child: InkWell(
|
||||
onTap: () => _onCardTap(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,
|
||||
),
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
// Determine number of columns dynamically
|
||||
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4);
|
||||
double cardWidth = (constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
|
||||
|
||||
return Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
alignment: WrapAlignment.end,
|
||||
children: stats
|
||||
.map((stat) => _buildFinanceModuleCard(stat, projectSelected, cardWidth))
|
||||
.toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildFinanceModuleCard(
|
||||
_FinanceStatItem stat, bool isProjectSelected, double width) {
|
||||
return Opacity(
|
||||
opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected
|
||||
child: IgnorePointer(
|
||||
ignoring: !isProjectSelected,
|
||||
child: InkWell(
|
||||
onTap: () => _onCardTap(stat, isProjectSelected),
|
||||
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),
|
||||
),
|
||||
MySpacing.height(4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
stat.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
),
|
||||
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,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onCardTap(_FinanceStatItem statItem, bool isEnabled) {
|
||||
if (!isEnabled) {
|
||||
Get.defaultDialog(
|
||||
title: "No Project Selected",
|
||||
middleText: "Please select a project before accessing this section.",
|
||||
confirm: ElevatedButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text("OK"),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Get.toNamed(statItem.route);
|
||||
}
|
||||
void _onCardTap(_FinanceStatItem statItem, bool isEnabled) {
|
||||
if (!isEnabled) {
|
||||
Get.defaultDialog(
|
||||
title: "No Project Selected",
|
||||
middleText: "Please select a project before accessing this section.",
|
||||
confirm: ElevatedButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text("OK"),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Navigate to the card's specific route
|
||||
Get.toNamed(statItem.route);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _FinanceStatItem {
|
||||
final IconData icon;
|
||||
@ -265,3 +287,9 @@ class _FinanceStatItem {
|
||||
|
||||
_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