made chnages into dynamic menus

This commit is contained in:
Vaibhav Surve 2025-11-12 17:58:33 +05:30
parent c8a8d45c66
commit 9f72f689c5
5 changed files with 398 additions and 363 deletions

View File

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

View File

@ -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(

View File

@ -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,
}; };
} }
} }

View File

@ -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,267 +258,45 @@ 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;
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; final isProjectSelected = projectController.selectedProject != null;
// Keep previous stat items (icons, title, routes) return Opacity(
final stats = [ opacity: isProjectSelected ? 1.0 : 0.4,
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, child: IgnorePointer(
DashboardScreen.attendanceRoute), ignoring: !isProjectSelected,
_StatItem(LucideIcons.users, "Employees", contentTheme.warning, child: ClipRRect(
DashboardScreen.employeesRoute), borderRadius: BorderRadius.circular(5),
_StatItem(LucideIcons.logs, "Daily Task Planning", contentTheme.info, child: SizedBox(
DashboardScreen.dailyTasksRoute), height: 400,
_StatItem(LucideIcons.list_todo, "Daily Progress Report", child: AttendanceDashboardChart(),
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 { /// ---------------- Dashboard Card Models ----------------
class _DashboardStatItem {
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);
} }

View File

@ -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 dont have access to the Finance section.", "You dont 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,103 +169,115 @@ 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"),
];
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) { // --- Log the routing info ---
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4); debugPrint(
double cardWidth = "[Finance Card] ID: ${menu.id}, Title: ${menu.name}, Route: ${menu.mobileLink}");
(constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
return Wrap( return _FinanceStatItem(
spacing: 6, meta.icon,
runSpacing: 6, menu.name,
alignment: WrapAlignment.end, meta.color,
children: stats menu.mobileLink, // Each card navigates to its own route
.map((stat) => );
_buildFinanceModuleCard(stat, projectSelected, cardWidth)) }).toList();
.toList(),
);
});
}
Widget _buildFinanceModuleCard( final projectSelected = projectController.selectedProject != null;
_FinanceStatItem stat, bool isProjectSelected, double width) {
final bool isEnabled = isProjectSelected;
return Opacity( return LayoutBuilder(builder: (context, constraints) {
opacity: isEnabled ? 1.0 : 0.4, // Determine number of columns dynamically
child: IgnorePointer( int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4);
ignoring: !isEnabled, double cardWidth = (constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
child: InkWell(
onTap: () => _onCardTap(stat, isEnabled), return Wrap(
borderRadius: BorderRadius.circular(5), spacing: 6,
child: MyCard.bordered( runSpacing: 6,
width: width, alignment: WrapAlignment.end,
height: 60, children: stats
paddingAll: 4, .map((stat) => _buildFinanceModuleCard(stat, projectSelected, cardWidth))
borderRadiusAll: 5, .toList(),
border: Border.all(color: Colors.grey.withOpacity(0.15)), );
child: Column( });
mainAxisAlignment: MainAxisAlignment.center, }
children: [
Container( Widget _buildFinanceModuleCard(
padding: const EdgeInsets.all(4), _FinanceStatItem stat, bool isProjectSelected, double width) {
decoration: BoxDecoration( return Opacity(
color: stat.color.withOpacity(0.1), opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected
borderRadius: BorderRadius.circular(4), child: IgnorePointer(
), ignoring: !isProjectSelected,
child: Icon( child: InkWell(
stat.icon, onTap: () => _onCardTap(stat, isProjectSelected),
size: 16, borderRadius: BorderRadius.circular(5),
color: stat.color, 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), child: Icon(
Flexible( stat.icon,
child: Text( size: 16,
stat.title, color: stat.color,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10,
overflow: TextOverflow.ellipsis,
),
maxLines: 2,
softWrap: true,
),
), ),
], ),
), 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) { void _onCardTap(_FinanceStatItem statItem, bool isEnabled) {
if (!isEnabled) { if (!isEnabled) {
Get.defaultDialog( Get.defaultDialog(
title: "No Project Selected", title: "No Project Selected",
middleText: "Please select a project before accessing this section.", middleText: "Please select a project before accessing this section.",
confirm: ElevatedButton( confirm: ElevatedButton(
onPressed: () => Get.back(), onPressed: () => Get.back(),
child: const Text("OK"), child: const Text("OK"),
), ),
); );
} else { } else {
Get.toNamed(statItem.route); // Navigate to the card's specific route
} Get.toNamed(statItem.route);
} }
} }
}
class _FinanceStatItem { class _FinanceStatItem {
final IconData icon; final IconData icon;
@ -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);
}