marco.pms.mobileapp/lib/view/dashboard/dashboard_screen.dart
Vaibhav Surve 8fb32c7c8e Refactor dashboard screen layout and improve loading state handling
- Simplified initialization of DynamicMenuController.
- Added loading skeleton for employee quick action cards.
- Removed daily task planning and daily progress report from card order.
- Adjusted grid layout parameters for better responsiveness.
- Cleaned up code formatting for improved readability.
2025-12-03 17:08:25 +05:30

583 lines
20 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:get/get.dart';
import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart';
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/avatar.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/model/attendance/attendence_action_button.dart';
import 'package:on_field_work/model/attendance/log_details_view.dart';
import 'package:on_field_work/view/layouts/layout.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
final DashboardController dashboardController =
Get.put(DashboardController(), permanent: true);
final AttendanceController attendanceController =
Get.put(AttendanceController());
final DynamicMenuController menuController = Get.put(DynamicMenuController());
final ProjectController projectController = Get.find<ProjectController>();
bool hasMpin = true;
@override
void initState() {
super.initState();
_checkMpinStatus();
}
Future<void> _checkMpinStatus() async {
hasMpin = await LocalStorage.getIsMpin();
if (mounted) {
setState(() {});
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
Widget _cardWrapper({required Widget child}) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.black12.withOpacity(.04)),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(.05),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: child,
);
}
Widget _sectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.black87,
),
),
);
}
// ---------------------------------------------------------------------------
// Quick Actions
// ---------------------------------------------------------------------------
Widget _quickActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Quick Action'),
Obx(() {
if (dashboardController.isLoadingEmployees.value) {
// Show loading skeleton
return SkeletonLoaders.attendanceQuickCardSkeleton();
}
final employees = dashboardController.employees;
final employee = employees.isNotEmpty ? employees.first : null;
if (employee == null) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
gradient: LinearGradient(
colors: [
contentTheme.primary.withOpacity(0.3),
contentTheme.primary.withOpacity(0.6),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: const Text(
'No attendance data available',
style: TextStyle(color: Colors.white),
),
);
}
// Actual employee quick action card
final bool isCheckedIn = employee.checkIn != null;
final bool isCheckedOut = employee.checkOut != null;
final String statusText = !isCheckedIn
? 'Check In Pending'
: isCheckedIn && !isCheckedOut
? 'Checked In'
: 'Checked Out';
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
gradient: LinearGradient(
colors: [
contentTheme.primary.withOpacity(0.3),
contentTheme.primary.withOpacity(0.6),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 30,
),
MySpacing.width(10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleSmall(
employee.name,
fontWeight: 600,
color: Colors.white,
),
MyText.labelSmall(
employee.designation,
fontWeight: 500,
color: Colors.white70,
),
],
),
),
MyText.bodySmall(
statusText,
fontWeight: 600,
color: Colors.white,
),
],
),
const SizedBox(height: 12),
Text(
!isCheckedIn
? 'You are not checked-in yet. Please check-in to start your work.'
: !isCheckedOut
? 'You are currently checked-in. Don\'t forget to check-out after your work.'
: 'You have checked-out for today.',
style: const TextStyle(
color: Colors.white70,
fontSize: 13,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AttendanceActionButton(
employee: employee,
attendanceController: attendanceController,
),
if (isCheckedIn) ...[
MySpacing.width(8),
AttendanceLogViewButton(
employee: employee,
attendanceController: attendanceController,
),
],
],
),
],
),
);
}),
],
);
}
// ---------------------------------------------------------------------------
// Dashboard Modules
// ---------------------------------------------------------------------------
Widget _dashboardModules() {
return Obx(() {
if (menuController.isLoading.value) {
return SkeletonLoaders.dashboardCardsSkeleton(
maxWidth: MediaQuery.of(context).size.width,
);
}
final bool projectSelected = projectController.selectedProject != null;
// these are String constants from permission_constants.dart
final List<String> cardOrder = [
MenuItems.attendance,
MenuItems.employees,
MenuItems.directory,
MenuItems.finance,
MenuItems.documents,
MenuItems.serviceProjects,
MenuItems.infraProjects,
];
final Map<String, _DashboardCardMeta> meta = {
MenuItems.attendance:
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
MenuItems.employees:
_DashboardCardMeta(LucideIcons.users, contentTheme.warning),
MenuItems.directory:
_DashboardCardMeta(LucideIcons.folder, contentTheme.info),
MenuItems.finance:
_DashboardCardMeta(LucideIcons.wallet, contentTheme.info),
MenuItems.documents:
_DashboardCardMeta(LucideIcons.file_text, contentTheme.info),
MenuItems.serviceProjects:
_DashboardCardMeta(LucideIcons.package, contentTheme.info),
MenuItems.infraProjects:
_DashboardCardMeta(LucideIcons.building_2, contentTheme.primary),
};
final Map<String, dynamic> allowed = {
for (final m in menuController.menuItems)
if (m.available && meta.containsKey(m.id)) m.id: m,
};
final List<String> filtered =
cardOrder.where((id) => allowed.containsKey(id)).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Modules',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.black87,
),
),
if (!projectSelected)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'Select Project',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
),
],
),
),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 15,
mainAxisSpacing: 8,
childAspectRatio: 1.8,
),
itemCount: filtered.length,
itemBuilder: (context, index) {
final String id = filtered[index];
final item = allowed[id]!;
final _DashboardCardMeta cardMeta = meta[id]!;
final bool isEnabled =
item.name == 'Attendance' ? true : projectSelected;
return GestureDetector(
onTap: () {
if (!isEnabled) {
Get.snackbar(
'Required',
'Please select a project first',
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(16),
backgroundColor: Colors.black87,
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
} else {
Get.toNamed(item.mobileLink);
}
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isEnabled
? Colors.black12.withOpacity(0.06)
: Colors.transparent,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
cardMeta.icon,
size: 20,
color:
isEnabled ? cardMeta.color : Colors.grey.shade300,
),
const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Text(
item.name,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 10,
fontWeight:
isEnabled ? FontWeight.w600 : FontWeight.w400,
color: isEnabled
? Colors.black87
: Colors.grey.shade400,
height: 1.2,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
},
),
],
);
});
}
// ---------------------------------------------------------------------------
// Project Selector
// ---------------------------------------------------------------------------
Widget _projectSelector() {
return Obx(() {
final bool isLoading = projectController.isLoading.value;
final bool expanded = projectController.isProjectSelectionExpanded.value;
final projects = projectController.projects;
final String? selectedId = projectController.selectedProjectId.value;
if (isLoading) {
return SkeletonLoaders.dashboardCardsSkeleton(
maxWidth: MediaQuery.of(context).size.width,
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Project'),
GestureDetector(
onTap: () => projectController.isProjectSelectionExpanded.toggle(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.black12.withOpacity(.15)),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(.04),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
const Icon(
Icons.work_outline,
color: Colors.blue,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
projects
.firstWhereOrNull(
(p) => p.id == selectedId,
)
?.name ??
'Select Project',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
Icon(
expanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 26,
color: Colors.black54,
),
],
),
),
),
if (expanded) _projectDropdownList(projects, selectedId),
],
);
});
}
Widget _projectDropdownList(List projects, String? selectedId) {
return Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.black12.withOpacity(.2)),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(.07),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.33,
),
child: Column(
children: [
TextField(
decoration: InputDecoration(
hintText: 'Search project...',
isDense: true,
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
),
),
),
const SizedBox(height: 10),
Expanded(
child: ListView.builder(
itemCount: projects.length,
itemBuilder: (_, index) {
final project = projects[index];
return RadioListTile<String>(
dense: true,
value: project.id,
groupValue: selectedId,
onChanged: (value) {
if (value != null) {
projectController.updateSelectedProject(value);
projectController.isProjectSelectionExpanded.value =
false;
}
},
title: Text(project.name),
);
},
),
),
],
),
);
}
// ---------------------------------------------------------------------------
// Build
// ---------------------------------------------------------------------------
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xfff5f6fa),
body: Layout(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_projectSelector(),
MySpacing.height(20),
_quickActions(),
MySpacing.height(20),
_dashboardModules(),
MySpacing.height(20),
_sectionTitle('Reports & Analytics'),
_cardWrapper(
child: ExpenseTypeReportChart(),
),
_cardWrapper(
child: ExpenseByStatusWidget(
controller: dashboardController,
),
),
_cardWrapper(
child: MonthlyExpenseDashboardChart(),
),
MySpacing.height(20),
],
),
),
),
);
}
}
class _DashboardCardMeta {
final IconData icon;
final Color color;
const _DashboardCardMeta(this.icon, this.color);
}