feat: Redesign Dashboard Overview Widgets with enhanced metrics display and loading states
This commit is contained in:
parent
781a8dabaf
commit
b5d8d41e42
@ -1,268 +1,414 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||||
|
|
||||||
|
// Assuming these exist in the project
|
||||||
|
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_card.dart';
|
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/controller/dashboard/dashboard_controller.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart'; // import MyText
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
|
// Redesigned Dashboard Overview Widgets
|
||||||
class DashboardOverviewWidgets {
|
class DashboardOverviewWidgets {
|
||||||
static final DashboardController dashboardController =
|
static final DashboardController dashboardController =
|
||||||
Get.find<DashboardController>();
|
Get.find<DashboardController>();
|
||||||
|
|
||||||
static const _titleTextStyle = TextStyle(
|
// Design tokens
|
||||||
|
static const _titleStyle = TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.black87,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const _subtitleStyle = TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.black54,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const _metricStyle = TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.black87,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const _percentStyle = TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const _subtitleTextStyle = TextStyle(
|
static final NumberFormat _comma = NumberFormat.decimalPattern();
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const _infoNumberTextStyle = TextStyle(
|
// Color palette with accessible contrast
|
||||||
fontSize: 20,
|
static const Color _primaryA = Color(0xFF1565C0); // Blue 800
|
||||||
fontWeight: FontWeight.bold,
|
static const Color _accentA = Color(0xFF2E7D32); // Green 800
|
||||||
color: Colors.black87,
|
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);
|
||||||
|
|
||||||
static const _infoNumberGreenTextStyle = TextStyle(
|
// Public API: Teams overview card
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
);
|
|
||||||
static final NumberFormat _commaFormatter = NumberFormat.decimalPattern();
|
|
||||||
|
|
||||||
/// Teams Overview Card without chart, labels & values in rows
|
|
||||||
static Widget teamsOverview() {
|
static Widget teamsOverview() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (dashboardController.isTeamsLoading.value) {
|
if (dashboardController.isTeamsLoading.value) {
|
||||||
return _loadingSkeletonCard("Teams");
|
return _skeletonCard(title: "Teams");
|
||||||
}
|
}
|
||||||
|
|
||||||
final total = dashboardController.totalEmployees.value;
|
final total = dashboardController.totalEmployees.value;
|
||||||
final inToday = dashboardController.inToday.value;
|
final inToday = dashboardController.inToday.value.clamp(0, total);
|
||||||
|
final absent = (total - inToday).clamp(0, total);
|
||||||
|
final percent = total > 0 ? inToday / total : 0.0;
|
||||||
|
|
||||||
return LayoutBuilder(
|
final hasData = total > 0;
|
||||||
builder: (context, constraints) {
|
|
||||||
final cardWidth = constraints.maxWidth > 400
|
|
||||||
? (constraints.maxWidth / 2) - 10
|
|
||||||
: constraints.maxWidth;
|
|
||||||
|
|
||||||
return SizedBox(
|
final data = hasData
|
||||||
width: cardWidth,
|
? [
|
||||||
child: MyCard(
|
_ChartData('In Today', inToday.toDouble(), _accentA),
|
||||||
borderRadiusAll: 5,
|
_ChartData('Absent', absent.toDouble(), _muted),
|
||||||
paddingAll: 20,
|
]
|
||||||
child: Column(
|
: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
_ChartData('No Data', 1.0, _hint),
|
||||||
children: [
|
_ChartData('No Data', 1.0, _hint),
|
||||||
Row(
|
];
|
||||||
children: [
|
|
||||||
const Icon(Icons.group,
|
return _MetricCard(
|
||||||
color: Colors.blueAccent, size: 26),
|
icon: Icons.group,
|
||||||
MySpacing.width(8),
|
iconColor: _primaryA,
|
||||||
MyText("Teams", style: _titleTextStyle),
|
title: "Teams",
|
||||||
],
|
subtitle: hasData ? "Attendance today" : "Awaiting data",
|
||||||
),
|
chart: _SemiDonutChart(
|
||||||
MySpacing.height(16),
|
percentLabel: "${(percent * 100).toInt()}%",
|
||||||
// Labels in one row
|
data: data,
|
||||||
Row(
|
startAngle: 270,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
endAngle: 90,
|
||||||
children: [
|
showLegend: false,
|
||||||
MyText("Total Employees", style: _subtitleTextStyle),
|
),
|
||||||
MyText("In Today", style: _subtitleTextStyle),
|
footer: _TwoColumnKpis(
|
||||||
],
|
leftLabel: "Total",
|
||||||
),
|
leftValue: _comma.format(total),
|
||||||
MySpacing.height(4),
|
rightLabel: "In Today",
|
||||||
// Values in one row
|
rightValue: _comma.format(inToday),
|
||||||
Row(
|
rightColor: _accentA,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
),
|
||||||
children: [
|
|
||||||
MyText(_commaFormatter.format(total),
|
|
||||||
style: _infoNumberTextStyle),
|
|
||||||
MyText(_commaFormatter.format(inToday),
|
|
||||||
style: _infoNumberGreenTextStyle.copyWith(
|
|
||||||
color: Colors.green[700])),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tasks Overview Card
|
// Public API: Tasks overview card
|
||||||
static Widget tasksOverview() {
|
static Widget tasksOverview() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (dashboardController.isTasksLoading.value) {
|
if (dashboardController.isTasksLoading.value) {
|
||||||
return _loadingSkeletonCard("Tasks");
|
return _skeletonCard(title: "Tasks");
|
||||||
}
|
}
|
||||||
|
|
||||||
final total = dashboardController.totalTasks.value;
|
final total = dashboardController.totalTasks.value;
|
||||||
final completed = dashboardController.completedTasks.value;
|
final completed =
|
||||||
final remaining = total - completed;
|
dashboardController.completedTasks.value.clamp(0, total);
|
||||||
final double percent = total > 0 ? completed / total : 0.0;
|
final remaining = (total - completed).clamp(0, total);
|
||||||
|
final percent = total > 0 ? completed / total : 0.0;
|
||||||
|
|
||||||
// Task colors
|
final hasData = total > 0;
|
||||||
const completedColor = Color(0xFF64B5F6);
|
|
||||||
const remainingColor =Color(0xFFE57373);
|
|
||||||
|
|
||||||
final List<_ChartData> pieData = [
|
final data = hasData
|
||||||
_ChartData('Completed', completed.toDouble(), completedColor),
|
? [
|
||||||
_ChartData('Remaining', remaining.toDouble(), remainingColor),
|
_ChartData('Completed', completed.toDouble(), _primaryA),
|
||||||
];
|
_ChartData('Remaining', remaining.toDouble(), _warnA),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
_ChartData('Completed', 1.0, _hint),
|
||||||
|
_ChartData('Remaining', 1.0, _hint),
|
||||||
|
];
|
||||||
|
|
||||||
return LayoutBuilder(
|
return _MetricCard(
|
||||||
builder: (context, constraints) {
|
icon: Icons.task_alt,
|
||||||
final cardWidth =
|
iconColor: _primaryA,
|
||||||
constraints.maxWidth < 300 ? constraints.maxWidth : 300.0;
|
title: "Tasks",
|
||||||
|
subtitle: hasData ? "Completion status" : "Awaiting data",
|
||||||
return SizedBox(
|
chart: _SemiDonutChart(
|
||||||
width: cardWidth,
|
percentLabel: "${(percent * 100).toInt()}%",
|
||||||
child: MyCard(
|
data: data,
|
||||||
borderRadiusAll: 5,
|
startAngle: 270,
|
||||||
paddingAll: 20,
|
endAngle: 90,
|
||||||
child: Column(
|
showLegend: true,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: [
|
footer: _TwoColumnKpis(
|
||||||
// Icon + Title
|
leftLabel: "Total",
|
||||||
Row(
|
leftValue: _comma.format(total),
|
||||||
children: [
|
rightLabel: "Completed",
|
||||||
const Icon(Icons.task_alt,
|
rightValue: _comma.format(completed),
|
||||||
color: completedColor, size: 26),
|
rightColor: _primaryA,
|
||||||
MySpacing.width(8),
|
),
|
||||||
MyText("Tasks", style: _titleTextStyle),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
|
|
||||||
// Main Row: Bigger Pie Chart + Full-Color Info Boxes
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// Pie Chart Column (Bigger)
|
|
||||||
SizedBox(
|
|
||||||
height: 140,
|
|
||||||
width: 140,
|
|
||||||
child: SfCircularChart(
|
|
||||||
annotations: <CircularChartAnnotation>[
|
|
||||||
CircularChartAnnotation(
|
|
||||||
widget: MyText(
|
|
||||||
"${(percent * 100).toInt()}%",
|
|
||||||
style: _infoNumberGreenTextStyle.copyWith(
|
|
||||||
fontSize: 20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
series: <PieSeries<_ChartData, String>>[
|
|
||||||
PieSeries<_ChartData, String>(
|
|
||||||
dataSource: pieData,
|
|
||||||
xValueMapper: (_ChartData data, _) =>
|
|
||||||
data.category,
|
|
||||||
yValueMapper: (_ChartData data, _) => data.value,
|
|
||||||
pointColorMapper: (_ChartData data, _) =>
|
|
||||||
data.color,
|
|
||||||
dataLabelSettings:
|
|
||||||
const DataLabelSettings(isVisible: false),
|
|
||||||
radius: '100%',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.width(16),
|
|
||||||
|
|
||||||
// Info Boxes Column (Full Color)
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_infoBoxFullColor(
|
|
||||||
"Completed", completed, completedColor),
|
|
||||||
MySpacing.height(8),
|
|
||||||
_infoBoxFullColor(
|
|
||||||
"Remaining", remaining, remainingColor),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Full-color info box
|
// Skeleton loading card
|
||||||
static Widget _infoBoxFullColor(String label, int value, Color bgColor) {
|
static Widget _skeletonCard({required String title}) {
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: bgColor, // full color
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
MyText(_commaFormatter.format(value),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
)),
|
|
||||||
MySpacing.height(2),
|
|
||||||
MyText(label,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.white, // text in white for contrast
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loading Skeleton Card
|
|
||||||
static Widget _loadingSkeletonCard(String title) {
|
|
||||||
return LayoutBuilder(builder: (context, constraints) {
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
final cardWidth =
|
final width = constraints.maxWidth.clamp(220.0, 480.0);
|
||||||
constraints.maxWidth < 200 ? constraints.maxWidth : 200.0;
|
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: cardWidth,
|
width: width,
|
||||||
child: MyCard(
|
child: MyCard(
|
||||||
borderRadiusAll: 5,
|
borderRadiusAll: 5,
|
||||||
paddingAll: 20,
|
paddingAll: 16,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_loadingBar(width: 100),
|
_Skeleton.line(width: 120, height: 16),
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
_loadingBar(width: 80),
|
_Skeleton.line(width: 80, height: 12),
|
||||||
MySpacing.height(12),
|
MySpacing.height(16),
|
||||||
_loadingBar(width: double.infinity, height: 12),
|
_Skeleton.block(height: 120),
|
||||||
|
MySpacing.height(16),
|
||||||
|
_Skeleton.line(width: double.infinity, height: 12),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Widget _loadingBar(
|
// Composable: Metric Card scaffold
|
||||||
{double width = double.infinity, double height = 16}) {
|
class _MetricCard extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final Color iconColor;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final Widget chart;
|
||||||
|
final Widget footer;
|
||||||
|
|
||||||
|
const _MetricCard({
|
||||||
|
required this.icon,
|
||||||
|
required this.iconColor,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.chart,
|
||||||
|
required this.footer,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
|
final maxW = constraints.maxWidth;
|
||||||
|
final clampedW = maxW.clamp(260.0, 560.0);
|
||||||
|
final dense = clampedW < 340;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: clampedW,
|
||||||
|
child: MyCard(
|
||||||
|
borderRadiusAll: 5,
|
||||||
|
paddingAll: dense ? 14 : 16,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_IconBadge(icon: icon, color: iconColor),
|
||||||
|
MySpacing.width(10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MyText(title,
|
||||||
|
style: DashboardOverviewWidgets._titleStyle),
|
||||||
|
MySpacing.height(2),
|
||||||
|
MyText(subtitle,
|
||||||
|
style: DashboardOverviewWidgets._subtitleStyle),
|
||||||
|
MySpacing.height(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: SizedBox(
|
||||||
|
height: dense ? 110 : 80,
|
||||||
|
child: chart,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(12),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
footer,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composable: Two-column KPIs
|
||||||
|
class _TwoColumnKpis extends StatelessWidget {
|
||||||
|
final String leftLabel;
|
||||||
|
final String leftValue;
|
||||||
|
final String rightLabel;
|
||||||
|
final String rightValue;
|
||||||
|
final Color rightColor;
|
||||||
|
|
||||||
|
const _TwoColumnKpis({
|
||||||
|
required this.leftLabel,
|
||||||
|
required this.leftValue,
|
||||||
|
required this.rightLabel,
|
||||||
|
required this.rightValue,
|
||||||
|
required this.rightColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composable: Semi-donut chart with annotation
|
||||||
|
class _SemiDonutChart extends StatelessWidget {
|
||||||
|
final String percentLabel;
|
||||||
|
final List<_ChartData> data;
|
||||||
|
final int startAngle;
|
||||||
|
final int endAngle;
|
||||||
|
final bool showLegend;
|
||||||
|
|
||||||
|
const _SemiDonutChart({
|
||||||
|
required this.percentLabel,
|
||||||
|
required this.data,
|
||||||
|
required this.startAngle,
|
||||||
|
required this.endAngle,
|
||||||
|
this.showLegend = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get _hasData =>
|
||||||
|
data.isNotEmpty &&
|
||||||
|
data.any((d) => d.color != DashboardOverviewWidgets._hint);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final chartData = _hasData
|
||||||
|
? data
|
||||||
|
: [
|
||||||
|
// Single grey slice for empty data
|
||||||
|
_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),
|
||||||
|
),
|
||||||
|
annotations: <CircularChartAnnotation>[
|
||||||
|
CircularChartAnnotation(
|
||||||
|
// Keep small to avoid pushing layout
|
||||||
|
height: '0%',
|
||||||
|
width: '0%',
|
||||||
|
widget: Center(
|
||||||
|
child: MyText(
|
||||||
|
percentLabel,
|
||||||
|
style: DashboardOverviewWidgets._percentStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
series: <DoughnutSeries<_ChartData, String>>[
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small UI parts
|
||||||
|
class _IconBadge extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
const _IconBadge({required this.icon, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: DashboardOverviewWidgets._bgSoft,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 22),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Skeleton {
|
||||||
|
static Widget line({double width = double.infinity, double height = 14}) {
|
||||||
return Container(
|
return Container(
|
||||||
height: height,
|
|
||||||
width: width,
|
width: width,
|
||||||
|
height: height,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.shade300,
|
color: Colors.grey.shade300,
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget block({double height = 120}) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: height,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -272,6 +418,5 @@ class _ChartData {
|
|||||||
final String category;
|
final String category;
|
||||||
final double value;
|
final double value;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
_ChartData(this.category, this.value, this.color);
|
_ChartData(this.category, this.value, this.color);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user