368 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			368 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:flutter/material.dart';
 | |
| 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/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';
 | |
| 
 | |
| 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 expenseMainPageRoute = "/dashboard/expense-main-page";
 | |
|   static const String documentMainPageRoute = "/dashboard/document-main-page";
 | |
| 
 | |
|   @override
 | |
|   State<DashboardScreen> createState() => _DashboardScreenState();
 | |
| }
 | |
| 
 | |
| class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
 | |
|   final DashboardController dashboardController =
 | |
|       Get.put(DashboardController(), permanent: true);
 | |
|   final DynamicMenuController menuController = Get.put(DynamicMenuController());
 | |
| 
 | |
|   bool hasMpin = true;
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     _checkMpinStatus();
 | |
|   }
 | |
| 
 | |
|   Future<void> _checkMpinStatus() async {
 | |
|     hasMpin = await LocalStorage.getIsMpin();
 | |
|     if (mounted) setState(() {});
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Layout(
 | |
|       child: SingleChildScrollView(
 | |
|         padding: const EdgeInsets.all(10),
 | |
|         child: Column(
 | |
|           crossAxisAlignment: CrossAxisAlignment.start,
 | |
|           children: [
 | |
|             _buildDashboardStats(context),
 | |
|             MySpacing.height(24),
 | |
|             SizedBox(
 | |
|               width: double.infinity,
 | |
|               child: DashboardOverviewWidgets.teamsOverview(),
 | |
|             ),
 | |
|             MySpacing.height(24),
 | |
|             SizedBox(
 | |
|               width: double.infinity,
 | |
|               child: DashboardOverviewWidgets.tasksOverview(),
 | |
|             ),
 | |
|             MySpacing.height(24),
 | |
|             _buildAttendanceChartSection(),
 | |
|             MySpacing.height(24),
 | |
|             _buildProjectProgressChartSection(),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /// Project Progress Chart Section
 | |
|   Widget _buildProjectProgressChartSection() {
 | |
|     return Obx(() {
 | |
|       if (dashboardController.projectChartData.isEmpty) {
 | |
|         return const Padding(
 | |
|           padding: EdgeInsets.all(16),
 | |
|           child: Center(
 | |
|             child: Text("No project progress data available."),
 | |
|           ),
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       return ClipRRect(
 | |
|         borderRadius: BorderRadius.circular(5),
 | |
|         child: SizedBox(
 | |
|           height: 400,
 | |
|           child: ProjectProgressChart(
 | |
|             data: dashboardController.projectChartData,
 | |
|           ),
 | |
|         ),
 | |
|       );
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /// Attendance Chart Section
 | |
|   Widget _buildAttendanceChartSection() {
 | |
|     return Obx(() {
 | |
|       final isAttendanceAllowed = menuController.isMenuAllowed("Attendance");
 | |
| 
 | |
|       if (!isAttendanceAllowed) {
 | |
|         // 🚫 Don't render anything if attendance menu is not allowed
 | |
|         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.badge_dollar_sign, "Expense", contentTheme.info,
 | |
|             DashboardScreen.expenseMainPageRoute),
 | |
|         _StatItem(LucideIcons.file_text, "Documents", contentTheme.info,
 | |
|             DashboardScreen.documentMainPageRoute),
 | |
|       ];
 | |
| 
 | |
|       // 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) => _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 String title;
 | |
|   final Color color;
 | |
|   final String route;
 | |
| 
 | |
|   _StatItem(this.icon, this.title, this.color, this.route);
 | |
| }
 |