import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:marco/model/dashboard/project_progress_model.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; class ProjectProgressChart extends StatelessWidget { final List data; final DashboardController controller = Get.find(); ProjectProgressChart({super.key, required this.data}); // ================= Flat Colors ================= static const List _flatColors = [ Color(0xFFE57373), Color(0xFF64B5F6), Color(0xFF81C784), Color(0xFFFFB74D), Color(0xFFBA68C8), Color(0xFFFF8A65), Color(0xFF4DB6AC), Color(0xFFA1887F), Color(0xFFDCE775), Color(0xFF9575CD), Color(0xFF7986CB), Color(0xFFAED581), Color(0xFFFF7043), Color(0xFF4FC3F7), Color(0xFFFFD54F), Color(0xFF90A4AE), Color(0xFFE573BB), Color(0xFF81D4FA), Color(0xFFBCAAA4), Color(0xFFA5D6A7), Color(0xFFCE93D8), Color(0xFFFF8A65), Color(0xFF80CBC4), Color(0xFFFFF176), Color(0xFF90CAF9), Color(0xFFE0E0E0), Color(0xFFF48FB1), Color(0xFFA1887F), Color(0xFFB0BEC5), Color(0xFF81C784), Color(0xFFFFB74D), Color(0xFF64B5F6), ]; static final NumberFormat _commaFormatter = NumberFormat.decimalPattern(); Color _getTaskColor(String taskName) { final index = taskName.hashCode % _flatColors.length; return _flatColors[index]; } @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; return Obx(() { final isChartView = controller.projectIsChartView.value; final selectedRange = controller.projectSelectedRange.value; return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.04), blurRadius: 6, spreadRadius: 1, offset: Offset(0, 2), ), ], ), padding: EdgeInsets.symmetric( vertical: 16, horizontal: screenWidth < 600 ? 8 : 24, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(selectedRange, isChartView, screenWidth), const SizedBox(height: 14), Expanded( child: LayoutBuilder( builder: (context, constraints) => AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: data.isEmpty ? _buildNoDataMessage() : isChartView ? _buildChart(constraints.maxHeight) : _buildTable(constraints.maxHeight, screenWidth), ), ), ), ], ), ); }); } Widget _buildHeader( String selectedRange, bool isChartView, double screenWidth) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium('Project Progress', fontWeight: 700), MyText.bodySmall('Planned vs Completed', color: Colors.grey.shade700), ], ), ), ToggleButtons( borderRadius: BorderRadius.circular(5), borderColor: Colors.grey, fillColor: Colors.blueAccent.withOpacity(0.15), selectedBorderColor: Colors.blueAccent, selectedColor: Colors.blueAccent, color: Colors.grey, constraints: BoxConstraints( minHeight: 30, minWidth: (screenWidth < 400 ? 28 : 36), ), isSelected: [isChartView, !isChartView], onPressed: (index) { controller.projectIsChartView.value = index == 0; }, children: const [ Icon(Icons.bar_chart_rounded, size: 15), Icon(Icons.table_chart, size: 15), ], ), ], ), const SizedBox(height: 6), Row( children: [ _buildRangeButton("7D", selectedRange), _buildRangeButton("15D", selectedRange), _buildRangeButton("30D", selectedRange), _buildRangeButton("3M", selectedRange), _buildRangeButton("6M", selectedRange), ], ), ], ); } Widget _buildRangeButton(String label, String selectedRange) { return Padding( padding: const EdgeInsets.only(right: 4.0), child: ChoiceChip( label: Text(label, style: const TextStyle(fontSize: 12)), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 0), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, selected: selectedRange == label, onSelected: (_) => controller.updateProjectRange(label), selectedColor: Colors.blueAccent.withOpacity(0.15), backgroundColor: Colors.grey.shade200, labelStyle: TextStyle( color: selectedRange == label ? Colors.blueAccent : Colors.black87, fontWeight: selectedRange == label ? FontWeight.w600 : FontWeight.normal, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), side: BorderSide( color: selectedRange == label ? Colors.blueAccent : Colors.grey.shade300, ), ), ), ); } Widget _buildChart(double height) { final nonZeroData = data.where((d) => d.planned != 0 || d.completed != 0).toList(); if (nonZeroData.isEmpty) { return _buildNoDataContainer(height); } return Container( height: height > 280 ? 280 : height, padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.blueGrey.shade50, borderRadius: BorderRadius.circular(5), ), child: SfCartesianChart( tooltipBehavior: TooltipBehavior(enable: true), legend: Legend(isVisible: true, position: LegendPosition.bottom), // ✅ Use CategoryAxis so only nonZeroData dates show up primaryXAxis: CategoryAxis( majorGridLines: const MajorGridLines(width: 0), axisLine: const AxisLine(width: 0), labelRotation: 0, ), primaryYAxis: NumericAxis( labelFormat: '{value}', axisLine: const AxisLine(width: 0), majorTickLines: const MajorTickLines(size: 0), ), series: [ ColumnSeries( name: 'Planned', dataSource: nonZeroData, xValueMapper: (d, _) => DateFormat('MMM d').format(d.date), yValueMapper: (d, _) => d.planned, color: _getTaskColor('Planned'), dataLabelSettings: DataLabelSettings( isVisible: true, builder: (data, point, series, pointIndex, seriesIndex) { final value = seriesIndex == 0 ? (data as ChartTaskData).planned : (data as ChartTaskData).completed; return Text( _commaFormatter.format(value), style: const TextStyle(fontSize: 11), ); }, ), ), ColumnSeries( name: 'Completed', dataSource: nonZeroData, xValueMapper: (d, _) => DateFormat('MMM d').format(d.date), yValueMapper: (d, _) => d.completed, color: _getTaskColor('Completed'), dataLabelSettings: DataLabelSettings( isVisible: true, builder: (data, point, series, pointIndex, seriesIndex) { final value = seriesIndex == 0 ? (data as ChartTaskData).planned : (data as ChartTaskData).completed; return Text( _commaFormatter.format(value), style: const TextStyle(fontSize: 11), ); }, ), ), ], ), ); } Widget _buildTable(double maxHeight, double screenWidth) { final containerHeight = maxHeight > 300 ? 300.0 : maxHeight; final nonZeroData = data.where((d) => d.planned != 0 || d.completed != 0).toList(); if (nonZeroData.isEmpty) { return _buildNoDataContainer(containerHeight); } return Container( height: containerHeight, padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(5), color: Colors.grey.shade50, ), child: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( scrollDirection: Axis.horizontal, child: ConstrainedBox( constraints: BoxConstraints(minWidth: constraints.maxWidth), child: SingleChildScrollView( scrollDirection: Axis.vertical, child: DataTable( columnSpacing: screenWidth < 600 ? 16 : 36, headingRowHeight: 44, headingRowColor: MaterialStateProperty.all( Colors.blueAccent.withOpacity(0.08)), headingTextStyle: const TextStyle( fontWeight: FontWeight.bold, color: Colors.black87), columns: const [ DataColumn(label: Text('Date')), DataColumn(label: Text('Planned')), DataColumn(label: Text('Completed')), ], rows: nonZeroData.map((task) { return DataRow( cells: [ DataCell(Text(DateFormat('d MMM').format(task.date))), DataCell(Text( '${task.planned}', style: TextStyle(color: _getTaskColor('Planned')), )), DataCell(Text( '${task.completed}', style: TextStyle(color: _getTaskColor('Completed')), )), ], ); }).toList(), ), ), ), ); }, ), ); } Widget _buildNoDataContainer(double height) { return Container( height: height > 280 ? 280 : height, decoration: BoxDecoration( color: Colors.blueGrey.shade50, borderRadius: BorderRadius.circular(5), ), child: const Center( child: Text( 'No project progress data for the selected range.', style: TextStyle(fontSize: 14, color: Colors.grey), textAlign: TextAlign.center, ), ), ); } Widget _buildNoDataMessage() { return SizedBox( height: 180, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.info_outline, color: Colors.grey.shade400, size: 54), const SizedBox(height: 10), MyText.bodyMedium( 'No project progress data available for the selected range.', textAlign: TextAlign.center, color: Colors.grey.shade500, ), ], ), ), ); } }