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/helpers/widgets/my_text.dart'; // Redesigned Dashboard Overview Widgets class DashboardOverviewWidgets { static final DashboardController dashboardController = Get.find(); // 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 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); // Public API: Teams overview card static Widget teamsOverview() { return Obx(() { if (dashboardController.isTeamsLoading.value) { return _skeletonCard(title: "Teams"); } final total = dashboardController.totalEmployees.value; final inToday = dashboardController.inToday.value.clamp(0, total); final absent = (total - inToday).clamp(0, total); final percent = total > 0 ? inToday / total : 0.0; final hasData = total > 0; 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, ), ); }); } // Public API: Tasks overview card static Widget tasksOverview() { return Obx(() { if (dashboardController.isTasksLoading.value) { return _skeletonCard(title: "Tasks"); } final total = dashboardController.totalTasks.value; final completed = dashboardController.completedTasks.value.clamp(0, total); final remaining = (total - completed).clamp(0, total); 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), ]; 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, ), ); }); } // Skeleton loading card static Widget _skeletonCard({required String title}) { return LayoutBuilder(builder: (context, constraints) { final width = constraints.maxWidth.clamp(220.0, 480.0); return SizedBox( width: width, child: MyCard( borderRadiusAll: 5, paddingAll: 16, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _Skeleton.line(width: 120, height: 16), MySpacing.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), ], ), ), ); }); } } // 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( width: width, height: height, decoration: BoxDecoration( color: Colors.grey.shade300, 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), ), ); } } class _ChartData { final String category; final double value; final Color color; _ChartData(this.category, this.value, this.color); }