refactor: Remove loading skeletons from attendance and project progress charts for improved performance

This commit is contained in:
Vaibhav Surve 2025-09-25 18:35:39 +05:30
parent b5d8d41e42
commit fd7c338c05
4 changed files with 99 additions and 149 deletions

View File

@ -4,7 +4,6 @@ import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:marco/controller/dashboard/dashboard_controller.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
class AttendanceDashboardChart extends StatelessWidget { class AttendanceDashboardChart extends StatelessWidget {
AttendanceDashboardChart({Key? key}) : super(key: key); AttendanceDashboardChart({Key? key}) : super(key: key);
@ -58,12 +57,9 @@ class AttendanceDashboardChart extends StatelessWidget {
return Obx(() { return Obx(() {
final isChartView = _controller.attendanceIsChartView.value; final isChartView = _controller.attendanceIsChartView.value;
final selectedRange = _controller.attendanceSelectedRange.value; final selectedRange = _controller.attendanceSelectedRange.value;
final isLoading = _controller.isAttendanceLoading.value;
final filteredData = _getFilteredData(); final filteredData = _getFilteredData();
if (isLoading) {
return SkeletonLoaders.buildLoadingSkeleton();
}
return Container( return Container(
decoration: _containerDecoration, decoration: _containerDecoration,

View File

@ -9,12 +9,11 @@ import 'package:marco/helpers/widgets/my_card.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/my_text.dart';
// Redesigned Dashboard Overview Widgets
class DashboardOverviewWidgets { class DashboardOverviewWidgets {
static final DashboardController dashboardController = static final DashboardController dashboardController =
Get.find<DashboardController>(); Get.find<DashboardController>();
// Design tokens // Text styles
static const _titleStyle = TextStyle( static const _titleStyle = TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@ -42,15 +41,15 @@ class DashboardOverviewWidgets {
static final NumberFormat _comma = NumberFormat.decimalPattern(); static final NumberFormat _comma = NumberFormat.decimalPattern();
// Color palette with accessible contrast // Colors
static const Color _primaryA = Color(0xFF1565C0); // Blue 800 static const Color _primaryA = Color(0xFF1565C0); // Blue
static const Color _accentA = Color(0xFF2E7D32); // Green 800 static const Color _accentA = Color(0xFF2E7D32); // Green
static const Color _warnA = Color(0xFFC62828); // Red 800 static const Color _warnA = Color(0xFFC62828); // Red
static const Color _muted = Color(0xFF9E9E9E); // Grey 500 static const Color _muted = Color(0xFF9E9E9E); // Grey
static const Color _hint = Color(0xFFBDBDBD); // Grey 400 static const Color _hint = Color(0xFFBDBDBD); // Light Grey
static const Color _bgSoft = Color(0xFFF7F8FA); static const Color _bgSoft = Color(0xFFF7F8FA); // Light background
// Public API: Teams overview card // --- TEAMS OVERVIEW ---
static Widget teamsOverview() { static Widget teamsOverview() {
return Obx(() { return Obx(() {
if (dashboardController.isTeamsLoading.value) { if (dashboardController.isTeamsLoading.value) {
@ -63,7 +62,6 @@ class DashboardOverviewWidgets {
final percent = total > 0 ? inToday / total : 0.0; final percent = total > 0 ? inToday / total : 0.0;
final hasData = total > 0; final hasData = total > 0;
final data = hasData final data = hasData
? [ ? [
_ChartData('In Today', inToday.toDouble(), _accentA), _ChartData('In Today', inToday.toDouble(), _accentA),
@ -71,7 +69,6 @@ class DashboardOverviewWidgets {
] ]
: [ : [
_ChartData('No Data', 1.0, _hint), _ChartData('No Data', 1.0, _hint),
_ChartData('No Data', 1.0, _hint),
]; ];
return _MetricCard( return _MetricCard(
@ -86,18 +83,21 @@ class DashboardOverviewWidgets {
endAngle: 90, endAngle: 90,
showLegend: false, showLegend: false,
), ),
footer: _TwoColumnKpis( footer: _SingleColumnKpis(
leftLabel: "Total", stats: {
leftValue: _comma.format(total), "In Today": _comma.format(inToday),
rightLabel: "In Today", "Absent": _comma.format(absent),
rightValue: _comma.format(inToday), },
rightColor: _accentA, colors: {
"In Today": _accentA,
"Absent": _muted,
},
), ),
); );
}); });
} }
// Public API: Tasks overview card // --- TASKS OVERVIEW ---
static Widget tasksOverview() { static Widget tasksOverview() {
return Obx(() { return Obx(() {
if (dashboardController.isTasksLoading.value) { if (dashboardController.isTasksLoading.value) {
@ -111,15 +111,13 @@ class DashboardOverviewWidgets {
final percent = total > 0 ? completed / total : 0.0; final percent = total > 0 ? completed / total : 0.0;
final hasData = total > 0; final hasData = total > 0;
final data = hasData final data = hasData
? [ ? [
_ChartData('Completed', completed.toDouble(), _primaryA), _ChartData('Completed', completed.toDouble(), _primaryA),
_ChartData('Remaining', remaining.toDouble(), _warnA), _ChartData('Remaining', remaining.toDouble(), _warnA),
] ]
: [ : [
_ChartData('Completed', 1.0, _hint), _ChartData('No Data', 1.0, _hint),
_ChartData('Remaining', 1.0, _hint),
]; ];
return _MetricCard( return _MetricCard(
@ -132,20 +130,23 @@ class DashboardOverviewWidgets {
data: data, data: data,
startAngle: 270, startAngle: 270,
endAngle: 90, endAngle: 90,
showLegend: true, showLegend: false,
), ),
footer: _TwoColumnKpis( footer: _SingleColumnKpis(
leftLabel: "Total", stats: {
leftValue: _comma.format(total), "Completed": _comma.format(completed),
rightLabel: "Completed", "Remaining": _comma.format(remaining),
rightValue: _comma.format(completed), },
rightColor: _primaryA, colors: {
"Completed": _primaryA,
"Remaining": _warnA,
},
), ),
); );
}); });
} }
// Skeleton loading card // Skeleton card
static Widget _skeletonCard({required String title}) { static Widget _skeletonCard({required String title}) {
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, constraints) {
final width = constraints.maxWidth.clamp(220.0, 480.0); final width = constraints.maxWidth.clamp(220.0, 480.0);
@ -172,7 +173,7 @@ class DashboardOverviewWidgets {
} }
} }
// Composable: Metric Card scaffold // --- METRIC CARD with chart on left, stats on right ---
class _MetricCard extends StatelessWidget { class _MetricCard extends StatelessWidget {
final IconData icon; final IconData icon;
final Color iconColor; final Color iconColor;
@ -204,8 +205,8 @@ class _MetricCard extends StatelessWidget {
paddingAll: dense ? 14 : 16, paddingAll: dense ? 14 : 16,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
// Header: icon + title + subtitle
Row( Row(
children: [ children: [
_IconBadge(icon: icon, color: iconColor), _IconBadge(icon: icon, color: iconColor),
@ -225,26 +226,21 @@ class _MetricCard extends StatelessWidget {
), ),
], ],
), ),
// Body: chart left, stats right
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
flex: 2, flex: 2,
child: SizedBox( child: SizedBox(
height: dense ? 110 : 80, height: dense ? 120 : 150,
child: chart, child: chart,
), ),
), ),
MySpacing.width(12), MySpacing.width(12),
Expanded( Expanded(
flex: 1, flex: 1,
child: Column( child: footer, // Stats stacked vertically
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
footer,
],
),
), ),
], ],
), ),
@ -256,51 +252,39 @@ class _MetricCard extends StatelessWidget {
} }
} }
// Composable: Two-column KPIs // --- SINGLE COLUMN KPIs (stacked vertically) ---
class _TwoColumnKpis extends StatelessWidget { class _SingleColumnKpis extends StatelessWidget {
final String leftLabel; final Map<String, String> stats;
final String leftValue; final Map<String, Color>? colors;
final String rightLabel;
final String rightValue;
final Color rightColor;
const _TwoColumnKpis({ const _SingleColumnKpis({required this.stats, this.colors});
required this.leftLabel,
required this.leftValue,
required this.rightLabel,
required this.rightValue,
required this.rightColor,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: stats.entries.map((entry) {
final color = colors != null && colors!.containsKey(entry.key)
? colors![entry.key]!
: DashboardOverviewWidgets._metricStyle.color;
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( MyText(entry.key, style: DashboardOverviewWidgets._subtitleStyle),
mainAxisAlignment: MainAxisAlignment.spaceBetween, MyText(entry.value,
children: [
MyText(leftLabel, style: DashboardOverviewWidgets._subtitleStyle),
MyText(rightLabel, style: DashboardOverviewWidgets._subtitleStyle),
],
),
MySpacing.height(4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText(leftValue, style: DashboardOverviewWidgets._metricStyle),
MyText(
rightValue,
style: DashboardOverviewWidgets._metricStyle style: DashboardOverviewWidgets._metricStyle
.copyWith(color: rightColor), .copyWith(color: color)),
),
], ],
), ),
], );
}).toList(),
); );
} }
} }
// Composable: Semi-donut chart with annotation // --- SEMI DONUT CHART ---
class _SemiDonutChart extends StatelessWidget { class _SemiDonutChart extends StatelessWidget {
final String percentLabel; final String percentLabel;
final List<_ChartData> data; final List<_ChartData> data;
@ -324,57 +308,43 @@ class _SemiDonutChart extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final chartData = _hasData final chartData = _hasData
? data ? data
: [ : [_ChartData('No Data', 1.0, DashboardOverviewWidgets._hint)];
// Single grey slice for empty data
_ChartData('No Data', 1.0, DashboardOverviewWidgets._hint),
];
return SfCircularChart( return SfCircularChart(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
centerX: '50%', centerY: '65%', // pull donut up
centerY: '50%', legend: Legend(isVisible: showLegend && _hasData),
legend: Legend(
isVisible: showLegend && _hasData,
position: LegendPosition.bottom,
overflowMode: LegendItemOverflowMode.wrap,
// Optional: tighter legend text reduces needed width
textStyle: const TextStyle(fontSize: 12, color: Colors.black87),
),
annotations: <CircularChartAnnotation>[ annotations: <CircularChartAnnotation>[
CircularChartAnnotation( CircularChartAnnotation(
// Keep small to avoid pushing layout
height: '0%',
width: '0%',
widget: Center( widget: Center(
child: MyText( child: MyText(percentLabel, style: DashboardOverviewWidgets._percentStyle),
percentLabel,
style: DashboardOverviewWidgets._percentStyle,
),
), ),
), ),
], ],
series: <DoughnutSeries<_ChartData, String>>[ series: <DoughnutSeries<_ChartData, String>>[
DoughnutSeries<_ChartData, String>( DoughnutSeries<_ChartData, String>(
dataSource: chartData, dataSource: chartData,
xValueMapper: (_ChartData d, _) => d.category, xValueMapper: (d, _) => d.category,
yValueMapper: (_ChartData d, _) => d.value, yValueMapper: (d, _) => d.value,
pointColorMapper: (_ChartData d, _) => d.color, pointColorMapper: (d, _) => d.color,
startAngle: startAngle, startAngle: startAngle,
endAngle: endAngle, endAngle: endAngle,
radius: '100%', radius: '80%',
innerRadius: '72%', innerRadius: '65%',
strokeWidth: 0, // avoids white stroke showing as gap strokeWidth: 0,
dataLabelSettings: const DataLabelSettings(isVisible: false), dataLabelSettings: const DataLabelSettings(isVisible: false),
), ),
], ],
); );
} }
} }
// Small UI parts // --- ICON BADGE ---
class _IconBadge extends StatelessWidget { class _IconBadge extends StatelessWidget {
final IconData icon; final IconData icon;
final Color color; final Color color;
const _IconBadge({required this.icon, required this.color}); const _IconBadge({required this.icon, required this.color});
@override @override
@ -390,6 +360,7 @@ class _IconBadge extends StatelessWidget {
} }
} }
// --- SKELETON ---
class _Skeleton { class _Skeleton {
static Widget line({double width = double.infinity, double height = 14}) { static Widget line({double width = double.infinity, double height = 14}) {
return Container( return Container(
@ -414,6 +385,7 @@ class _Skeleton {
} }
} }
// --- CHART DATA ---
class _ChartData { class _ChartData {
final String category; final String category;
final double value; final double value;

View File

@ -5,7 +5,6 @@ import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:marco/model/dashboard/project_progress_model.dart'; import 'package:marco/model/dashboard/project_progress_model.dart';
import 'package:marco/controller/dashboard/dashboard_controller.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
class ProjectProgressChart extends StatelessWidget { class ProjectProgressChart extends StatelessWidget {
final List<ChartTaskData> data; final List<ChartTaskData> data;
@ -62,7 +61,6 @@ class ProjectProgressChart extends StatelessWidget {
return Obx(() { return Obx(() {
final isChartView = controller.projectIsChartView.value; final isChartView = controller.projectIsChartView.value;
final selectedRange = controller.projectSelectedRange.value; final selectedRange = controller.projectSelectedRange.value;
final isLoading = controller.isProjectLoading.value;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -90,9 +88,7 @@ class ProjectProgressChart extends StatelessWidget {
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) => AnimatedSwitcher( builder: (context, constraints) => AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: isLoading child: data.isEmpty
? SkeletonLoaders.buildLoadingSkeleton()
: data.isEmpty
? _buildNoDataMessage() ? _buildNoDataMessage()
: isChartView : isChartView
? _buildChart(constraints.maxHeight) ? _buildChart(constraints.maxHeight)

View File

@ -13,7 +13,6 @@ 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/view/layouts/layout.dart';
import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart'; import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/helpers/widgets/dashbaord/dashboard_overview_widgets.dart'; import 'package:marco/helpers/widgets/dashbaord/dashboard_overview_widgets.dart';
class DashboardScreen extends StatefulWidget { class DashboardScreen extends StatefulWidget {
@ -85,12 +84,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
/// Project Progress Chart Section /// Project Progress Chart Section
Widget _buildProjectProgressChartSection() { Widget _buildProjectProgressChartSection() {
return Obx(() { return Obx(() {
if (dashboardController.isProjectLoading.value) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: SkeletonLoaders.chartSkeletonLoader(),
);
}
if (dashboardController.projectChartData.isEmpty) { if (dashboardController.projectChartData.isEmpty) {
return const Padding( return const Padding(
@ -116,14 +110,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
/// Attendance Chart Section /// Attendance Chart Section
Widget _buildAttendanceChartSection() { Widget _buildAttendanceChartSection() {
return Obx(() { return Obx(() {
if (menuController.isLoading.value) {
// Show Skeleton Loader Instead of CircularProgressIndicator
return Padding(
padding: const EdgeInsets.all(8.0),
child: SkeletonLoaders
.chartSkeletonLoader(), // <-- using the skeleton we built
);
}
final isAttendanceAllowed = menuController.isMenuAllowed("Attendance"); final isAttendanceAllowed = menuController.isMenuAllowed("Attendance");