refactor: Remove loading skeletons from attendance and project progress charts for improved performance
This commit is contained in:
parent
b5d8d41e42
commit
fd7c338c05
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user