diff --git a/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart b/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart index f887403..0b72adf 100644 --- a/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart +++ b/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart @@ -1,268 +1,414 @@ import 'package:flutter/material.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_spacing.dart'; -import 'package:marco/controller/dashboard/dashboard_controller.dart'; -import 'package:syncfusion_flutter_charts/charts.dart'; -import 'package:marco/helpers/widgets/my_text.dart'; // import MyText -import 'package:intl/intl.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +// Redesigned Dashboard Overview Widgets class DashboardOverviewWidgets { static final DashboardController dashboardController = Get.find(); - 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, fontWeight: FontWeight.w700, color: Colors.black87, ); - static const _subtitleTextStyle = TextStyle( - fontSize: 14, - color: Colors.grey, - ); + static final NumberFormat _comma = NumberFormat.decimalPattern(); - static const _infoNumberTextStyle = TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black87, - ); + // 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); - static const _infoNumberGreenTextStyle = TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ); - static final NumberFormat _commaFormatter = NumberFormat.decimalPattern(); - - /// Teams Overview Card without chart, labels & values in rows + // Public API: Teams overview card static Widget teamsOverview() { return Obx(() { if (dashboardController.isTeamsLoading.value) { - return _loadingSkeletonCard("Teams"); + return _skeletonCard(title: "Teams"); } 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( - builder: (context, constraints) { - final cardWidth = constraints.maxWidth > 400 - ? (constraints.maxWidth / 2) - 10 - : constraints.maxWidth; + final hasData = total > 0; - return SizedBox( - width: cardWidth, - child: MyCard( - borderRadiusAll: 5, - paddingAll: 20, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.group, - color: Colors.blueAccent, size: 26), - MySpacing.width(8), - MyText("Teams", style: _titleTextStyle), - ], - ), - MySpacing.height(16), - // Labels in one row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText("Total Employees", style: _subtitleTextStyle), - MyText("In Today", style: _subtitleTextStyle), - ], - ), - MySpacing.height(4), - // Values in one row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText(_commaFormatter.format(total), - style: _infoNumberTextStyle), - MyText(_commaFormatter.format(inToday), - style: _infoNumberGreenTextStyle.copyWith( - color: Colors.green[700])), - ], - ), - ], - ), - ), - ); - }, + final data = hasData + ? [ + _ChartData('In Today', inToday.toDouble(), _accentA), + _ChartData('Absent', absent.toDouble(), _muted), + ] + : [ + _ChartData('No Data', 1.0, _hint), + _ChartData('No Data', 1.0, _hint), + ]; + + return _MetricCard( + icon: Icons.group, + iconColor: _primaryA, + title: "Teams", + subtitle: hasData ? "Attendance today" : "Awaiting data", + chart: _SemiDonutChart( + percentLabel: "${(percent * 100).toInt()}%", + data: data, + startAngle: 270, + endAngle: 90, + showLegend: false, + ), + footer: _TwoColumnKpis( + leftLabel: "Total", + leftValue: _comma.format(total), + rightLabel: "In Today", + rightValue: _comma.format(inToday), + rightColor: _accentA, + ), ); }); } - /// Tasks Overview Card + // Public API: Tasks overview card static Widget tasksOverview() { return Obx(() { if (dashboardController.isTasksLoading.value) { - return _loadingSkeletonCard("Tasks"); + return _skeletonCard(title: "Tasks"); } final total = dashboardController.totalTasks.value; - final completed = dashboardController.completedTasks.value; - final remaining = total - completed; - final double percent = total > 0 ? completed / total : 0.0; + final completed = + dashboardController.completedTasks.value.clamp(0, total); + final remaining = (total - completed).clamp(0, total); + final percent = total > 0 ? completed / total : 0.0; - // Task colors - const completedColor = Color(0xFF64B5F6); - const remainingColor =Color(0xFFE57373); + final hasData = total > 0; - final List<_ChartData> pieData = [ - _ChartData('Completed', completed.toDouble(), completedColor), - _ChartData('Remaining', remaining.toDouble(), remainingColor), - ]; + final data = hasData + ? [ + _ChartData('Completed', completed.toDouble(), _primaryA), + _ChartData('Remaining', remaining.toDouble(), _warnA), + ] + : [ + _ChartData('Completed', 1.0, _hint), + _ChartData('Remaining', 1.0, _hint), + ]; - return LayoutBuilder( - builder: (context, constraints) { - final cardWidth = - constraints.maxWidth < 300 ? constraints.maxWidth : 300.0; - - return SizedBox( - width: cardWidth, - child: MyCard( - borderRadiusAll: 5, - paddingAll: 20, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Icon + Title - Row( - children: [ - const Icon(Icons.task_alt, - color: completedColor, size: 26), - 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( - widget: MyText( - "${(percent * 100).toInt()}%", - style: _infoNumberGreenTextStyle.copyWith( - fontSize: 20), - ), - ), - ], - series: >[ - 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), - ], - ), - ), - ], - ), - ], - ), - ), - ); - }, + return _MetricCard( + icon: Icons.task_alt, + iconColor: _primaryA, + title: "Tasks", + subtitle: hasData ? "Completion status" : "Awaiting data", + chart: _SemiDonutChart( + percentLabel: "${(percent * 100).toInt()}%", + data: data, + startAngle: 270, + endAngle: 90, + showLegend: true, + ), + footer: _TwoColumnKpis( + leftLabel: "Total", + leftValue: _comma.format(total), + rightLabel: "Completed", + rightValue: _comma.format(completed), + rightColor: _primaryA, + ), ); }); } - /// Full-color info box - static Widget _infoBoxFullColor(String label, int value, Color bgColor) { - 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) { + // Skeleton loading card + static Widget _skeletonCard({required String title}) { return LayoutBuilder(builder: (context, constraints) { - final cardWidth = - constraints.maxWidth < 200 ? constraints.maxWidth : 200.0; - + final width = constraints.maxWidth.clamp(220.0, 480.0); return SizedBox( - width: cardWidth, + width: width, child: MyCard( borderRadiusAll: 5, - paddingAll: 20, + paddingAll: 16, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _loadingBar(width: 100), + _Skeleton.line(width: 120, height: 16), MySpacing.height(12), - _loadingBar(width: 80), - MySpacing.height(12), - _loadingBar(width: double.infinity, height: 12), + _Skeleton.line(width: 80, height: 12), + MySpacing.height(16), + _Skeleton.block(height: 120), + MySpacing.height(16), + _Skeleton.line(width: double.infinity, height: 12), ], ), ), ); }); } +} - static Widget _loadingBar( - {double width = double.infinity, double height = 16}) { +// Composable: Metric Card scaffold +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( + // 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), + ), + ], + ); + } +} + +// 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( - height: height, width: width, + height: height, decoration: BoxDecoration( 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 double value; final Color color; - _ChartData(this.category, this.value, this.color); }