From fd7c338c05b5c840fb9d542591299d1f12f51804 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 25 Sep 2025 18:35:39 +0530 Subject: [PATCH] refactor: Remove loading skeletons from attendance and project progress charts for improved performance --- .../dashbaord/attendance_overview_chart.dart | 6 +- .../dashbaord/dashboard_overview_widgets.dart | 212 ++++++++---------- .../dashbaord/project_progress_chart.dart | 14 +- lib/view/dashboard/dashboard_screen.dart | 16 +- 4 files changed, 99 insertions(+), 149 deletions(-) diff --git a/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart index d9f06b2..4ec74b0 100644 --- a/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart +++ b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart @@ -4,7 +4,6 @@ import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; class AttendanceDashboardChart extends StatelessWidget { AttendanceDashboardChart({Key? key}) : super(key: key); @@ -58,12 +57,9 @@ class AttendanceDashboardChart extends StatelessWidget { return Obx(() { final isChartView = _controller.attendanceIsChartView.value; final selectedRange = _controller.attendanceSelectedRange.value; - final isLoading = _controller.isAttendanceLoading.value; final filteredData = _getFilteredData(); - if (isLoading) { - return SkeletonLoaders.buildLoadingSkeleton(); - } + return Container( decoration: _containerDecoration, diff --git a/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart b/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart index 0b72adf..fcebbd9 100644 --- a/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart +++ b/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart @@ -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_text.dart'; -// Redesigned Dashboard Overview Widgets class DashboardOverviewWidgets { static final DashboardController dashboardController = Get.find(); - // Design tokens + // Text styles static const _titleStyle = TextStyle( fontSize: 16, fontWeight: FontWeight.w700, @@ -42,15 +41,15 @@ class DashboardOverviewWidgets { static final NumberFormat _comma = NumberFormat.decimalPattern(); - // Color palette with accessible contrast - static const Color _primaryA = Color(0xFF1565C0); // Blue 800 - static const Color _accentA = Color(0xFF2E7D32); // Green 800 - static const Color _warnA = Color(0xFFC62828); // Red 800 - static const Color _muted = Color(0xFF9E9E9E); // Grey 500 - static const Color _hint = Color(0xFFBDBDBD); // Grey 400 - static const Color _bgSoft = Color(0xFFF7F8FA); + // Colors + static const Color _primaryA = Color(0xFF1565C0); // Blue + static const Color _accentA = Color(0xFF2E7D32); // Green + static const Color _warnA = Color(0xFFC62828); // Red + static const Color _muted = Color(0xFF9E9E9E); // Grey + static const Color _hint = Color(0xFFBDBDBD); // Light Grey + static const Color _bgSoft = Color(0xFFF7F8FA); // Light background - // Public API: Teams overview card + // --- TEAMS OVERVIEW --- static Widget teamsOverview() { return Obx(() { if (dashboardController.isTeamsLoading.value) { @@ -63,7 +62,6 @@ class DashboardOverviewWidgets { final percent = total > 0 ? inToday / total : 0.0; final hasData = total > 0; - final data = hasData ? [ _ChartData('In Today', inToday.toDouble(), _accentA), @@ -71,7 +69,6 @@ class DashboardOverviewWidgets { ] : [ _ChartData('No Data', 1.0, _hint), - _ChartData('No Data', 1.0, _hint), ]; return _MetricCard( @@ -86,18 +83,21 @@ class DashboardOverviewWidgets { endAngle: 90, showLegend: false, ), - footer: _TwoColumnKpis( - leftLabel: "Total", - leftValue: _comma.format(total), - rightLabel: "In Today", - rightValue: _comma.format(inToday), - rightColor: _accentA, + footer: _SingleColumnKpis( + stats: { + "In Today": _comma.format(inToday), + "Absent": _comma.format(absent), + }, + colors: { + "In Today": _accentA, + "Absent": _muted, + }, ), ); }); } - // Public API: Tasks overview card + // --- TASKS OVERVIEW --- static Widget tasksOverview() { return Obx(() { if (dashboardController.isTasksLoading.value) { @@ -111,15 +111,13 @@ class DashboardOverviewWidgets { final percent = total > 0 ? completed / total : 0.0; final hasData = total > 0; - final data = hasData ? [ _ChartData('Completed', completed.toDouble(), _primaryA), _ChartData('Remaining', remaining.toDouble(), _warnA), ] : [ - _ChartData('Completed', 1.0, _hint), - _ChartData('Remaining', 1.0, _hint), + _ChartData('No Data', 1.0, _hint), ]; return _MetricCard( @@ -132,20 +130,23 @@ class DashboardOverviewWidgets { data: data, startAngle: 270, endAngle: 90, - showLegend: true, + showLegend: false, ), - footer: _TwoColumnKpis( - leftLabel: "Total", - leftValue: _comma.format(total), - rightLabel: "Completed", - rightValue: _comma.format(completed), - rightColor: _primaryA, + footer: _SingleColumnKpis( + stats: { + "Completed": _comma.format(completed), + "Remaining": _comma.format(remaining), + }, + colors: { + "Completed": _primaryA, + "Remaining": _warnA, + }, ), ); }); } - // Skeleton loading card + // Skeleton card static Widget _skeletonCard({required String title}) { return LayoutBuilder(builder: (context, constraints) { 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 { final IconData icon; final Color iconColor; @@ -204,8 +205,8 @@ class _MetricCard extends StatelessWidget { paddingAll: dense ? 14 : 16, child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, children: [ + // Header: icon + title + subtitle Row( children: [ _IconBadge(icon: icon, color: iconColor), @@ -225,26 +226,21 @@ class _MetricCard extends StatelessWidget { ), ], ), + // Body: chart left, stats right Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 2, child: SizedBox( - height: dense ? 110 : 80, + height: dense ? 120 : 150, child: chart, ), ), MySpacing.width(12), Expanded( flex: 1, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - footer, - ], - ), + child: footer, // Stats stacked vertically ), ], ), @@ -256,51 +252,39 @@ class _MetricCard extends StatelessWidget { } } -// Composable: Two-column KPIs -class _TwoColumnKpis extends StatelessWidget { - final String leftLabel; - final String leftValue; - final String rightLabel; - final String rightValue; - final Color rightColor; +// --- SINGLE COLUMN KPIs (stacked vertically) --- +class _SingleColumnKpis extends StatelessWidget { + final Map stats; + final Map? colors; - const _TwoColumnKpis({ - required this.leftLabel, - required this.leftValue, - required this.rightLabel, - required this.rightValue, - required this.rightColor, - }); + const _SingleColumnKpis({required this.stats, this.colors}); @override Widget build(BuildContext context) { return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - 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 - .copyWith(color: rightColor), - ), - ], - ), - ], + 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: [ + MyText(entry.key, style: DashboardOverviewWidgets._subtitleStyle), + MyText(entry.value, + style: DashboardOverviewWidgets._metricStyle + .copyWith(color: color)), + ], + ), + ); + }).toList(), ); } } -// Composable: Semi-donut chart with annotation +// --- SEMI DONUT CHART --- class _SemiDonutChart extends StatelessWidget { final String percentLabel; final List<_ChartData> data; @@ -324,57 +308,43 @@ class _SemiDonutChart extends StatelessWidget { Widget build(BuildContext context) { final chartData = _hasData ? data - : [ - // Single grey slice for empty data - _ChartData('No Data', 1.0, DashboardOverviewWidgets._hint), - ]; + : [_ChartData('No Data', 1.0, DashboardOverviewWidgets._hint)]; - return SfCircularChart( - margin: EdgeInsets.zero, - centerX: '50%', - centerY: '50%', - 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), + return SfCircularChart( + margin: EdgeInsets.zero, + centerY: '65%', // pull donut up + legend: Legend(isVisible: showLegend && _hasData), + annotations: [ + CircularChartAnnotation( + widget: Center( + child: MyText(percentLabel, style: DashboardOverviewWidgets._percentStyle), ), - annotations: [ - CircularChartAnnotation( - // Keep small to avoid pushing layout - height: '0%', - width: '0%', - widget: Center( - child: MyText( - percentLabel, - style: DashboardOverviewWidgets._percentStyle, - ), - ), - ), - ], - series: >[ - DoughnutSeries<_ChartData, String>( - dataSource: chartData, - xValueMapper: (_ChartData d, _) => d.category, - yValueMapper: (_ChartData d, _) => d.value, - pointColorMapper: (_ChartData d, _) => d.color, - startAngle: startAngle, - endAngle: endAngle, - radius: '100%', - innerRadius: '72%', - strokeWidth: 0, // avoids white stroke showing as “gap” - dataLabelSettings: const DataLabelSettings(isVisible: false), - ), - ], - ); + ), + ], + series: >[ + DoughnutSeries<_ChartData, String>( + dataSource: chartData, + xValueMapper: (d, _) => d.category, + yValueMapper: (d, _) => d.value, + pointColorMapper: (d, _) => d.color, + startAngle: startAngle, + endAngle: endAngle, + radius: '80%', + innerRadius: '65%', + strokeWidth: 0, + dataLabelSettings: const DataLabelSettings(isVisible: false), + ), + ], +); + } } -// Small UI parts +// --- ICON BADGE --- class _IconBadge extends StatelessWidget { final IconData icon; final Color color; + const _IconBadge({required this.icon, required this.color}); @override @@ -390,6 +360,7 @@ class _IconBadge extends StatelessWidget { } } +// --- SKELETON --- class _Skeleton { static Widget line({double width = double.infinity, double height = 14}) { return Container( @@ -414,6 +385,7 @@ class _Skeleton { } } +// --- CHART DATA --- class _ChartData { final String category; final double value; diff --git a/lib/helpers/widgets/dashbaord/project_progress_chart.dart b/lib/helpers/widgets/dashbaord/project_progress_chart.dart index 0f8e28f..648fc75 100644 --- a/lib/helpers/widgets/dashbaord/project_progress_chart.dart +++ b/lib/helpers/widgets/dashbaord/project_progress_chart.dart @@ -5,7 +5,6 @@ import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:marco/model/dashboard/project_progress_model.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; class ProjectProgressChart extends StatelessWidget { final List data; @@ -62,7 +61,6 @@ class ProjectProgressChart extends StatelessWidget { return Obx(() { final isChartView = controller.projectIsChartView.value; final selectedRange = controller.projectSelectedRange.value; - final isLoading = controller.isProjectLoading.value; return Container( decoration: BoxDecoration( @@ -90,13 +88,11 @@ class ProjectProgressChart extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) => AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: isLoading - ? SkeletonLoaders.buildLoadingSkeleton() - : data.isEmpty - ? _buildNoDataMessage() - : isChartView - ? _buildChart(constraints.maxHeight) - : _buildTable(constraints.maxHeight, screenWidth), + child: data.isEmpty + ? _buildNoDataMessage() + : isChartView + ? _buildChart(constraints.maxHeight) + : _buildTable(constraints.maxHeight, screenWidth), ), ), ), diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 44f73ce..5f89aec 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -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/view/layouts/layout.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'; class DashboardScreen extends StatefulWidget { @@ -85,12 +84,7 @@ class _DashboardScreenState extends State with UIMixin { /// Project Progress Chart Section Widget _buildProjectProgressChartSection() { return Obx(() { - if (dashboardController.isProjectLoading.value) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: SkeletonLoaders.chartSkeletonLoader(), - ); - } + if (dashboardController.projectChartData.isEmpty) { return const Padding( @@ -116,14 +110,6 @@ class _DashboardScreenState extends State with UIMixin { /// Attendance Chart Section Widget _buildAttendanceChartSection() { 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");