import 'package:flutter/material.dart'; import 'package:get/get.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/model/dashboard/expense_type_report_model.dart'; import 'package:marco/helpers/utils/utils.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; class ExpenseTypeReportChart extends StatelessWidget { ExpenseTypeReportChart({Key? key}) : super(key: key); final DashboardController _controller = Get.find(); // Extended color palette for multiple projects static const List _flatColors = [ Color(0xFFE57373), // Red 300 Color(0xFF64B5F6), // Blue 300 Color(0xFF81C784), // Green 300 Color(0xFFFFB74D), // Orange 300 Color(0xFFBA68C8), // Purple 300 Color(0xFFFF8A65), // Deep Orange 300 Color(0xFF4DB6AC), // Teal 300 Color(0xFFA1887F), // Brown 400 Color(0xFFDCE775), // Lime 300 Color(0xFF9575CD), // Deep Purple 300 Color(0xFF7986CB), // Indigo 300 Color(0xFFAED581), // Light Green 300 Color(0xFFFF7043), // Deep Orange 400 Color(0xFF4FC3F7), // Light Blue 300 Color(0xFFFFD54F), // Amber 300 Color(0xFF90A4AE), // Blue Grey 300 Color(0xFFE573BB), // Pink 300 Color(0xFF81D4FA), // Light Blue 200 Color(0xFFBCAAA4), // Brown 300 Color(0xFFA5D6A7), // Green 300 Color(0xFFCE93D8), // Purple 200 Color(0xFFFF8A65), // Deep Orange 300 (repeat to fill) Color(0xFF80CBC4), // Teal 200 Color(0xFFFFF176), // Yellow 300 Color(0xFF90CAF9), // Blue 200 Color(0xFFE0E0E0), // Grey 300 Color(0xFFF48FB1), // Pink 200 Color(0xFFA1887F), // Brown 400 (repeat) Color(0xFFB0BEC5), // Blue Grey 200 Color(0xFF81C784), // Green 300 (repeat) Color(0xFFFFB74D), // Orange 300 (repeat) Color(0xFF64B5F6), // Blue 300 (repeat) ]; Color _getSeriesColor(int index) => _flatColors[index % _flatColors.length]; @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; final isMobile = screenWidth < 600; return Obx(() { final isLoading = _controller.isExpenseTypeReportLoading.value; final data = _controller.expenseTypeReportData.value; return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.05), blurRadius: 6, spreadRadius: 1, offset: const Offset(0, 2), ), ], ), padding: EdgeInsets.symmetric( vertical: isMobile ? 16 : 20, horizontal: isMobile ? 12 : 20, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Chart Header isLoading ? SkeletonLoaders.dateSkeletonLoader() : _ChartHeader(controller: _controller), const SizedBox(height: 12), // Date Range Picker isLoading ? Row( children: [ Expanded(child: SkeletonLoaders.dateSkeletonLoader()), const SizedBox(width: 8), Expanded(child: SkeletonLoaders.dateSkeletonLoader()), ], ) : _DateRangePicker(controller: _controller), const SizedBox(height: 16), // Chart Area SizedBox( height: isMobile ? 350 : 400, child: isLoading ? SkeletonLoaders.chartSkeletonLoader() : (data == null || data.report.isEmpty) ? const _NoDataMessage() : _ExpenseDonutChart( data: data, getSeriesColor: _getSeriesColor, isMobile: isMobile, ), ), ], ), ); }); } } // ----------------------------------------------------------------------------- // Chart Header // ----------------------------------------------------------------------------- class _ChartHeader extends StatelessWidget { const _ChartHeader({Key? key, required this.controller}) : super(key: key); final DashboardController controller; @override Widget build(BuildContext context) { return Obx(() { final data = controller.expenseTypeReportData.value; // Calculate total from totalApprovedAmount only final total = data?.report.fold( 0, (sum, e) => sum + e.totalApprovedAmount, ) ?? 0; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium('Project Expense Analytics', fontWeight: 700), const SizedBox(height: 2), MyText.bodySmall('Approved expenses by project', color: Colors.grey), ], ), ), if (total > 0) Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.blueAccent.withOpacity(0.15), borderRadius: BorderRadius.circular(5), border: Border.all(color: Colors.blueAccent, width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ MyText.bodySmall( 'Total Approved', color: Colors.blueAccent, fontSize: 10, ), MyText.bodyMedium( Utils.formatCurrency(total), color: Colors.blueAccent, fontWeight: 700, fontSize: 14, ), ], ), ), ], ); }); } } // ----------------------------------------------------------------------------- // Date Range Picker // ----------------------------------------------------------------------------- class _DateRangePicker extends StatelessWidget { const _DateRangePicker({Key? key, required this.controller}) : super(key: key); final DashboardController controller; Future _selectDate( BuildContext context, bool isStartDate, DateTime currentDate) async { final DateTime? picked = await showDatePicker( context: context, initialDate: currentDate, firstDate: DateTime(2020), lastDate: DateTime.now(), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light( primary: Colors.blueAccent, onPrimary: Colors.white, onSurface: Colors.black, ), ), child: child!, ); }, ); if (picked != null) { if (isStartDate) { controller.expenseReportStartDate.value = picked; } else { controller.expenseReportEndDate.value = picked; } } } @override Widget build(BuildContext context) { return Obx(() { final startDate = controller.expenseReportStartDate.value; final endDate = controller.expenseReportEndDate.value; return Row( children: [ _DateBox( label: 'Start Date', date: startDate, onTap: () => _selectDate(context, true, startDate), icon: Icons.calendar_today_outlined, ), const SizedBox(width: 8), _DateBox( label: 'End Date', date: endDate, onTap: () => _selectDate(context, false, endDate), icon: Icons.event_outlined, ), ], ); }); } } class _DateBox extends StatelessWidget { final String label; final DateTime date; final VoidCallback onTap; final IconData icon; const _DateBox({ Key? key, required this.label, required this.date, required this.onTap, required this.icon, }) : super(key: key); @override Widget build(BuildContext context) { return Expanded( child: Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(5), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: Colors.blueAccent.withOpacity(0.08), border: Border.all(color: Colors.blueAccent.withOpacity(0.3)), borderRadius: BorderRadius.circular(5), ), child: Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.blueAccent.withOpacity(0.15), borderRadius: BorderRadius.circular(4), ), child: Icon( icon, size: 14, color: Colors.blueAccent, ), ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 10, color: Colors.grey.shade600, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 2), Text( Utils.formatDate(date), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.blueAccent, ), overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ), ), ), ); } } // ----------------------------------------------------------------------------- // No Data Message // ----------------------------------------------------------------------------- class _NoDataMessage extends StatelessWidget { const _NoDataMessage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.donut_large_outlined, color: Colors.grey.shade400, size: 48), const SizedBox(height: 10), MyText.bodyMedium( 'No expense data available for this range.', textAlign: TextAlign.center, color: Colors.grey.shade500, ), ], ), ); } } // ----------------------------------------------------------------------------- // Donut Chart // ----------------------------------------------------------------------------- class _ExpenseDonutChart extends StatefulWidget { const _ExpenseDonutChart({ Key? key, required this.data, required this.getSeriesColor, required this.isMobile, }) : super(key: key); final ExpenseTypeReportData data; final Color Function(int index) getSeriesColor; final bool isMobile; @override State<_ExpenseDonutChart> createState() => _ExpenseDonutChartState(); } class _ExpenseDonutChartState extends State<_ExpenseDonutChart> { late TooltipBehavior _tooltipBehavior; late SelectionBehavior _selectionBehavior; @override void initState() { super.initState(); _tooltipBehavior = TooltipBehavior( enable: true, builder: (dynamic data, dynamic point, dynamic series, int pointIndex, int seriesIndex) { final total = widget.data.report .fold(0, (sum, e) => sum + e.totalApprovedAmount); final value = data.value as double; final percentage = total > 0 ? (value / total * 100) : 0; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: Colors.blueAccent, borderRadius: BorderRadius.circular(4), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( data.label, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w600), ), const SizedBox(height: 2), Text( Utils.formatCurrency(value), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w600), ), Text( '${percentage.toStringAsFixed(1)}%', style: const TextStyle( color: Colors.white70, fontWeight: FontWeight.w500, fontSize: 10), ), ], ), ); }, elevation: 4, animationDuration: 300, ); _selectionBehavior = SelectionBehavior( enable: true, selectedColor: Colors.white, selectedBorderColor: Colors.blueAccent, selectedBorderWidth: 3, unselectedOpacity: 0.5, ); } @override Widget build(BuildContext context) { // Create donut data from project items using totalApprovedAmount final List<_DonutData> donutData = widget.data.report .asMap() .entries .map((entry) => _DonutData( entry.value.projectName.isEmpty ? 'Project ${entry.key + 1}' : entry.value.projectName, entry.value.totalApprovedAmount, widget.getSeriesColor(entry.key), Icons.folder_outlined, )) .toList(); // Filter out zero values for cleaner visualization final filteredData = donutData.where((data) => data.value > 0).toList(); if (filteredData.isEmpty) { return const Center( child: Text( 'No approved expense data for the selected range.', textAlign: TextAlign.center, style: TextStyle(fontSize: 14, color: Colors.grey), ), ); } // Calculate total for center display final total = filteredData.fold(0, (sum, item) => sum + item.value); return Column( children: [ Expanded( child: SfCircularChart( margin: EdgeInsets.zero, legend: Legend( isVisible: true, position: LegendPosition.bottom, overflowMode: LegendItemOverflowMode.wrap, textStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ), iconHeight: 10, iconWidth: 10, itemPadding: widget.isMobile ? 6 : 10, padding: widget.isMobile ? 10 : 14, ), tooltipBehavior: _tooltipBehavior, // Center annotation showing total approved amount annotations: [ CircularChartAnnotation( widget: Container( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.check_circle_outline, color: Colors.green.shade600, size: widget.isMobile ? 28 : 32, ), const SizedBox(height: 6), Text( 'Total Approved', style: TextStyle( fontSize: widget.isMobile ? 11 : 12, color: Colors.grey.shade600, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Text( Utils.formatCurrency(total), style: TextStyle( fontSize: widget.isMobile ? 16 : 18, color: Colors.green.shade700, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 2), Text( '${filteredData.length} ${filteredData.length == 1 ? 'Project' : 'Projects'}', style: TextStyle( fontSize: widget.isMobile ? 9 : 10, color: Colors.grey.shade500, fontWeight: FontWeight.w500, ), ), ], ), ), ), ], series: >[ DoughnutSeries<_DonutData, String>( dataSource: filteredData, xValueMapper: (datum, _) => datum.label, yValueMapper: (datum, _) => datum.value, pointColorMapper: (datum, _) => datum.color, dataLabelMapper: (datum, _) { final amount = Utils.formatCurrency(datum.value); return widget.isMobile ? '$amount' : '${datum.label}\n$amount'; }, dataLabelSettings: DataLabelSettings( isVisible: true, labelPosition: ChartDataLabelPosition.outside, connectorLineSettings: ConnectorLineSettings( type: ConnectorType.curve, length: widget.isMobile ? '15%' : '18%', width: 1.5, color: Colors.grey.shade400, ), textStyle: TextStyle( fontSize: widget.isMobile ? 10 : 11, fontWeight: FontWeight.w700, color: Colors.black87, ), labelIntersectAction: LabelIntersectAction.shift, ), innerRadius: widget.isMobile ? '40%' : '45%', radius: widget.isMobile ? '75%' : '80%', explode: true, explodeAll: false, explodeIndex: 0, explodeOffset: '5%', explodeGesture: ActivationMode.singleTap, startAngle: 90, endAngle: 450, strokeColor: Colors.white, strokeWidth: 2.5, enableTooltip: true, animationDuration: 1000, selectionBehavior: _selectionBehavior, opacity: 0.95, ), ], ), ), if (!widget.isMobile) ...[ const SizedBox(height: 12), _ProjectSummary(donutData: filteredData), ], ], ); } } // ----------------------------------------------------------------------------- // Project Summary (Desktop only) // ----------------------------------------------------------------------------- class _ProjectSummary extends StatelessWidget { const _ProjectSummary({Key? key, required this.donutData}) : super(key: key); final List<_DonutData> donutData; @override Widget build(BuildContext context) { return Wrap( spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, children: donutData.map((data) { return Container( constraints: const BoxConstraints(minWidth: 120), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: data.color.withOpacity(0.15), borderRadius: BorderRadius.circular(5), border: Border.all( color: data.color.withOpacity(0.4), width: 1, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(data.icon, color: data.color, size: 18), const SizedBox(height: 4), Text( data.label, style: TextStyle( fontSize: 11, color: Colors.grey.shade700, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( Utils.formatCurrency(data.value), style: TextStyle( fontSize: 12, color: data.color, fontWeight: FontWeight.w700, ), ), ], ), ); }).toList(), ); } } class _DonutData { final String label; final double value; final Color color; final IconData icon; _DonutData(this.label, this.value, this.color, this.icon); }