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'; class ExpenseTypeReportChart extends StatelessWidget { ExpenseTypeReportChart({Key? key}) : super(key: key); final DashboardController _controller = Get.find(); 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; return Obx(() { final isLoading = _controller.isExpenseTypeReportLoading.value; final data = _controller.expenseTypeReportData.value; return Container( decoration: _containerDecoration, padding: EdgeInsets.symmetric( vertical: 16, horizontal: screenWidth < 600 ? 8 : 20, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _Header(), const SizedBox(height: 12), // 👇 replace Expanded with fixed height SizedBox( height: 350, // choose based on your design child: isLoading ? const Center(child: CircularProgressIndicator()) : data == null || data.report.isEmpty ? const _NoDataMessage() : _ExpenseChart( data: data, getSeriesColor: _getSeriesColor, ), ), ], ), ); }); } BoxDecoration get _containerDecoration => 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), ), ], ); } class _Header extends StatelessWidget { const _Header({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium('Expense Type Overview', fontWeight: 700), const SizedBox(height: 2), MyText.bodySmall( 'Project-wise approved, pending, rejected & processed expenses', color: Colors.grey), ], ); } } // No data class _NoDataMessage extends StatelessWidget { const _NoDataMessage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return SizedBox( height: 200, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.info_outline, color: Colors.grey.shade400, size: 48), const SizedBox(height: 10), MyText.bodyMedium( 'No expense data available.', textAlign: TextAlign.center, color: Colors.grey.shade500, ), ], ), ), ); } } // Chart class _ExpenseChart extends StatelessWidget { const _ExpenseChart({ Key? key, required this.data, required this.getSeriesColor, }) : super(key: key); final ExpenseTypeReportData data; final Color Function(int index) getSeriesColor; @override Widget build(BuildContext context) { final List> chartSeries = [ { 'name': 'Approved', 'color': getSeriesColor(0), 'yValue': (ExpenseTypeReportItem e) => e.totalApprovedAmount, }, { 'name': 'Pending', 'color': getSeriesColor(1), 'yValue': (ExpenseTypeReportItem e) => e.totalPendingAmount, }, { 'name': 'Rejected', 'color': getSeriesColor(2), 'yValue': (ExpenseTypeReportItem e) => e.totalRejectedAmount, }, { 'name': 'Processed', 'color': getSeriesColor(3), 'yValue': (ExpenseTypeReportItem e) => e.totalProcessedAmount, }, ]; return SfCartesianChart( tooltipBehavior: TooltipBehavior( enable: true, shared: true, builder: (data, point, series, pointIndex, seriesIndex) { final ExpenseTypeReportItem item = data; final value = chartSeries[seriesIndex]['yValue'](item); return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(6), ), child: Text( '${chartSeries[seriesIndex]['name']}: ${Utils.formatCurrency(value)}', style: const TextStyle(color: Colors.white, fontSize: 12), ), ); }, ), legend: Legend(isVisible: true, position: LegendPosition.bottom), primaryXAxis: CategoryAxis(labelRotation: 45), primaryYAxis: NumericAxis( // ✅ Format axis labels with Utils axisLabelFormatter: (AxisLabelRenderDetails details) { final num value = details.value; return ChartAxisLabel( Utils.formatCurrency(value), const TextStyle(fontSize: 10)); }, axisLine: const AxisLine(width: 0), majorGridLines: const MajorGridLines(width: 0.5), ), series: chartSeries.map((seriesInfo) { return ColumnSeries( dataSource: data.report, xValueMapper: (item, _) => item.projectName, yValueMapper: (item, _) => seriesInfo['yValue'](item), name: seriesInfo['name'], color: seriesInfo['color'], dataLabelSettings: const DataLabelSettings(isVisible: true), // ✅ Format data labels as well dataLabelMapper: (item, _) => Utils.formatCurrency(seriesInfo['yValue'](item)), ); }).toList(), ); } }