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.
This commit is contained in:
Vaibhav Surve 2025-12-03 17:08:25 +05:30
parent 3dfa6e5877
commit 8fb32c7c8e
2 changed files with 1237 additions and 1028 deletions

View File

@ -5,7 +5,8 @@ import 'package:on_field_work/helpers/utils/my_shadow.dart';
class SkeletonLoaders {
static Widget buildLoadingSkeleton() {
return SizedBox(
return ShimmerEffect(
child: SizedBox(
height: 360,
child: Column(
children: List.generate(5, (index) {
@ -30,10 +31,111 @@ class SkeletonLoaders {
);
}),
),
),
);
}
static Widget attendanceQuickCardSkeleton() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
gradient: LinearGradient(
colors: [
Colors.grey.shade300.withOpacity(0.3),
Colors.grey.shade300.withOpacity(0.6),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: ShimmerEffect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Row with avatar and texts
Row(
children: [
// Avatar
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.grey.shade400,
shape: BoxShape.circle,
),
),
MySpacing.width(10),
// Name + designation
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 12,
width: 100,
color: Colors.grey.shade400,
),
MySpacing.height(6),
Container(
height: 10,
width: 70,
color: Colors.grey.shade400,
),
],
),
),
// Status
Container(
height: 12,
width: 60,
color: Colors.grey.shade400,
),
],
),
const SizedBox(height: 12),
// Description
Container(
height: 10,
width: double.infinity,
color: Colors.grey.shade400,
),
MySpacing.height(6),
Container(
height: 10,
width: double.infinity,
color: Colors.grey.shade400,
),
const SizedBox(height: 12),
// Action buttons
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
height: 28,
width: 80,
decoration: BoxDecoration(
color: Colors.grey.shade400,
borderRadius: BorderRadius.circular(4),
),
),
MySpacing.width(8),
Container(
height: 28,
width: 28,
decoration: BoxDecoration(
color: Colors.grey.shade400,
shape: BoxShape.circle,
),
),
],
),
],
),
),
);
}
// Inside SkeletonLoaders class
static Widget dashboardCardsSkeleton({double? maxWidth}) {
return LayoutBuilder(builder: (context, constraints) {
double width = maxWidth ?? constraints.maxWidth;
@ -50,6 +152,7 @@ class SkeletonLoaders {
paddingAll: 4,
borderRadiusAll: 5,
border: Border.all(color: Colors.grey.withOpacity(0.15)),
child: ShimmerEffect(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -69,13 +172,13 @@ class SkeletonLoaders {
),
],
),
),
);
}),
);
});
}
// Inside SkeletonLoaders class
static Widget paymentRequestListSkeletonLoader() {
return ListView.separated(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
@ -83,7 +186,8 @@ class SkeletonLoaders {
separatorBuilder: (_, __) =>
Divider(color: Colors.grey.shade300, height: 20),
itemBuilder: (context, index) {
return Container(
return ShimmerEffect(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -163,12 +267,12 @@ class SkeletonLoaders {
),
],
),
),
);
},
);
}
// Add this inside SkeletonLoaders class
static Widget paymentRequestDetailSkeletonLoader() {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 30),
@ -179,6 +283,7 @@ class SkeletonLoaders {
paddingAll: 16,
borderRadiusAll: 8,
shadow: MyShadow(elevation: 3),
child: ShimmerEffect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -265,7 +370,8 @@ class SkeletonLoaders {
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Container(
height: 14,
@ -296,10 +402,10 @@ class SkeletonLoaders {
),
),
),
),
);
}
// Employee Detail Skeleton Loader
static Widget employeeDetailSkeletonLoader() {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(12, 20, 12, 80),
@ -312,6 +418,7 @@ class SkeletonLoaders {
paddingAll: 16,
margin: MySpacing.bottom(16),
shadow: MyShadow(elevation: 2),
child: ShimmerEffect(
child: Row(
children: [
// Avatar
@ -350,6 +457,7 @@ class SkeletonLoaders {
],
),
),
),
// Sections skeleton
...List.generate(
@ -361,6 +469,7 @@ class SkeletonLoaders {
paddingAll: 16,
margin: MySpacing.bottom(16),
shadow: MyShadow(elevation: 2),
child: ShimmerEffect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -387,8 +496,8 @@ class SkeletonLoaders {
...List.generate(
2,
(_) => Padding(
padding: const EdgeInsets.symmetric(
vertical: 12),
padding:
const EdgeInsets.symmetric(vertical: 12),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
@ -429,14 +538,15 @@ class SkeletonLoaders {
],
),
),
),
],
)),
),
),
],
),
);
}
// Daily Progress Planning - Infra (Expanded) Skeleton Loader
static Widget dailyProgressPlanningInfraSkeleton() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -446,6 +556,7 @@ class SkeletonLoaders {
paddingAll: 5,
margin: MySpacing.bottom(10),
shadow: MyShadow(elevation: 1.5),
child: ShimmerEffect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -505,7 +616,8 @@ class SkeletonLoaders {
height: 10,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4),
borderRadius:
BorderRadius.circular(4),
),
),
),
@ -521,12 +633,12 @@ class SkeletonLoaders {
),
],
),
),
);
}),
);
}
// Chart Skeleton Loader (Donut Chart)
static Widget chartSkeletonLoader() {
return MyCard.bordered(
paddingAll: 16,
@ -535,6 +647,7 @@ class SkeletonLoaders {
elevation: 1.5,
position: MyShadowPosition.bottom,
),
child: ShimmerEffect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -550,8 +663,7 @@ class SkeletonLoaders {
const SizedBox(height: 16),
// Donut Skeleton Placeholder
Expanded(
child: Center(
Center(
child: Container(
width: 180,
height: 180,
@ -561,7 +673,6 @@ class SkeletonLoaders {
),
),
),
),
const SizedBox(height: 16),
@ -582,22 +693,23 @@ class SkeletonLoaders {
),
],
),
),
);
}
// Date Skeleton Loader
static Widget dateSkeletonLoader() {
return Container(
return ShimmerEffect(
child: Container(
height: 14,
width: 90,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6),
),
),
);
}
// Expense By Status Skeleton Loader
static Widget expenseByStatusSkeletonLoader() {
return Container(
padding: const EdgeInsets.all(16),
@ -613,6 +725,7 @@ class SkeletonLoaders {
),
],
),
child: ShimmerEffect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -729,10 +842,10 @@ class SkeletonLoaders {
),
],
),
),
);
}
// Document List Skeleton Loader
static Widget documentSkeletonLoader() {
return Column(
children: List.generate(5, (index) {
@ -742,6 +855,7 @@ class SkeletonLoaders {
// Date placeholder
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: ShimmerEffect(
child: Container(
height: 12,
width: 80,
@ -751,6 +865,7 @@ class SkeletonLoaders {
),
),
),
),
// Document Card Skeleton
Container(
@ -767,6 +882,7 @@ class SkeletonLoaders {
),
],
),
child: ShimmerEffect(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -820,13 +936,13 @@ class SkeletonLoaders {
],
),
),
),
],
);
}),
);
}
// Document Details Card Skeleton Loader
static Widget documentDetailsSkeletonLoader() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
@ -848,6 +964,7 @@ class SkeletonLoaders {
),
],
),
child: ShimmerEffect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -929,12 +1046,14 @@ class SkeletonLoaders {
],
),
),
),
const SizedBox(height: 20),
// Versions section skeleton
Container(
margin: const EdgeInsets.symmetric(horizontal: 12),
child: ShimmerEffect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(3, (index) {
@ -983,12 +1102,12 @@ class SkeletonLoaders {
}),
),
),
),
],
),
);
}
// Employee List - Card Style
static Widget employeeListSkeletonLoader() {
return Column(
children: List.generate(4, (index) {
@ -997,6 +1116,7 @@ class SkeletonLoaders {
paddingAll: 10,
margin: MySpacing.bottom(12),
shadow: MyShadow(elevation: 3),
child: ShimmerEffect(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -1022,7 +1142,9 @@ class SkeletonLoaders {
color: Colors.grey.shade300),
MySpacing.width(8),
Container(
height: 12, width: 60, color: Colors.grey.shade300),
height: 12,
width: 60,
color: Colors.grey.shade300),
],
),
MySpacing.height(8),
@ -1054,16 +1176,17 @@ class SkeletonLoaders {
),
],
),
),
);
}),
);
}
// Employee List - Compact Collapsed Style
static Widget employeeListCollapsedSkeletonLoader() {
return MyCard.bordered(
borderRadiusAll: 4,
paddingAll: 8,
child: ShimmerEffect(
child: Column(
children: List.generate(4, (index) {
return Column(
@ -1127,16 +1250,17 @@ class SkeletonLoaders {
);
}),
),
),
);
}
// Daily Progress Report Header Loader
static Widget dailyProgressReportSkeletonLoader() {
return MyCard.bordered(
borderRadiusAll: 4,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8,
child: ShimmerEffect(
child: Column(
children: List.generate(3, (index) {
return Column(
@ -1158,11 +1282,10 @@ class SkeletonLoaders {
);
}),
),
),
);
}
// Daily Progress Planning (Collapsed View)
static Widget dailyProgressPlanningSkeletonCollapsedOnly() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -1172,6 +1295,7 @@ class SkeletonLoaders {
paddingAll: 16,
margin: MySpacing.bottom(12),
shadow: MyShadow(elevation: 3),
child: ShimmerEffect(
child: Row(
children: [
// Icon placeholder
@ -1200,6 +1324,7 @@ class SkeletonLoaders {
),
],
),
),
);
}),
);
@ -1208,11 +1333,12 @@ class SkeletonLoaders {
static Widget expenseListSkeletonLoader() {
return ListView.separated(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
itemCount: 6, // Show 6 skeleton items
itemCount: 6,
separatorBuilder: (_, __) =>
Divider(color: Colors.grey.shade300, height: 20),
itemBuilder: (context, index) {
return Column(
return ShimmerEffect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title and Amount
@ -1261,6 +1387,7 @@ class SkeletonLoaders {
],
),
],
),
);
},
);
@ -1275,6 +1402,7 @@ class SkeletonLoaders {
elevation: 1.5,
position: MyShadowPosition.bottom,
),
child: ShimmerEffect(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -1294,7 +1422,8 @@ class SkeletonLoaders {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(height: 12, width: 120, color: Colors.grey.shade300),
Container(
height: 12, width: 120, color: Colors.grey.shade300),
MySpacing.height(6),
Container(height: 10, width: 80, color: Colors.grey.shade300),
MySpacing.height(8),
@ -1339,9 +1468,11 @@ class SkeletonLoaders {
),
// Arrow
Icon(Icons.arrow_forward_ios, size: 14, color: Colors.grey.shade300),
Icon(Icons.arrow_forward_ios,
size: 14, color: Colors.grey.shade300),
],
),
),
);
}
@ -1354,6 +1485,7 @@ class SkeletonLoaders {
elevation: 1.5,
position: MyShadowPosition.bottom,
),
child: ShimmerEffect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -1396,6 +1528,88 @@ class SkeletonLoaders {
Container(height: 10, width: 120, color: Colors.grey.shade300),
],
),
),
);
}
}
/// A custom reusable Shimmer Effect widget.
/// This creates a gradient wave animation over any child widget.
class ShimmerEffect extends StatefulWidget {
final Widget child;
final Color baseColor;
final Color highlightColor;
ShimmerEffect({
Key? key,
required this.child,
Color? baseColor,
Color? highlightColor,
}) : baseColor = baseColor ?? Colors.grey.shade300,
highlightColor = highlightColor ?? Colors.grey.shade100,
super(key: key);
@override
State<ShimmerEffect> createState() => _ShimmerEffectState();
}
class _ShimmerEffectState extends State<ShimmerEffect>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2), // Adjust speed here
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: widget.child,
builder: (context, child) {
return ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (bounds) {
final gradient = LinearGradient(
colors: [
widget.baseColor,
widget.highlightColor,
widget.baseColor,
],
stops: const [0.1, 0.5, 0.9],
begin: const Alignment(-1.0, -0.3),
end: const Alignment(1.0, 0.3),
transform: _SlidingGradientTransform(_controller.value),
);
return gradient.createShader(bounds);
},
child: child,
);
},
);
}
}
class _SlidingGradientTransform extends GradientTransform {
final double percent;
const _SlidingGradientTransform(this.percent);
@override
Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
// This moves the gradient across the width of the widget
return Matrix4.translationValues(
(bounds.width * 2) * percent - bounds.width, 0, 0);
}
}

View File

@ -31,8 +31,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
Get.put(DashboardController(), permanent: true);
final AttendanceController attendanceController =
Get.put(AttendanceController());
final DynamicMenuController menuController =
Get.put(DynamicMenuController());
final DynamicMenuController menuController = Get.put(DynamicMenuController());
final ProjectController projectController = Get.find<ProjectController>();
bool hasMpin = true;
@ -97,6 +96,11 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
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;
@ -121,6 +125,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
);
}
// Actual employee quick action card
final bool isCheckedIn = employee.checkIn != null;
final bool isCheckedOut = employee.checkOut != null;
@ -233,8 +238,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
final List<String> cardOrder = [
MenuItems.attendance,
MenuItems.employees,
MenuItems.dailyTaskPlanning,
MenuItems.dailyProgressReport,
MenuItems.directory,
MenuItems.finance,
MenuItems.documents,
@ -247,10 +250,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
_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:
@ -314,10 +313,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
crossAxisCount: 3,
crossAxisSpacing: 15,
mainAxisSpacing: 8,
childAspectRatio: 1.15,
childAspectRatio: 1.8,
),
itemCount: filtered.length,
itemBuilder: (context, index) {
@ -367,9 +366,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
Icon(
cardMeta.icon,
size: 20,
color: isEnabled
? cardMeta.color
: Colors.grey.shade300,
color:
isEnabled ? cardMeta.color : Colors.grey.shade300,
),
const SizedBox(height: 6),
Padding(
@ -379,9 +377,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 10,
fontWeight: isEnabled
? FontWeight.w600
: FontWeight.w400,
fontWeight:
isEnabled ? FontWeight.w600 : FontWeight.w400,
color: isEnabled
? Colors.black87
: Colors.grey.shade400,
@ -424,11 +421,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
children: [
_sectionTitle('Project'),
GestureDetector(
onTap: () =>
projectController.isProjectSelectionExpanded.toggle(),
onTap: () => projectController.isProjectSelectionExpanded.toggle(),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),