import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../providers/reports_provider.dart'; import 'report_card_wrapper.dart'; /// Donut chart ticket counts per status with hover animation. class TicketsByStatusChart extends ConsumerStatefulWidget { const TicketsByStatusChart({super.key, this.repaintKey}); final GlobalKey? repaintKey; @override ConsumerState createState() => _TicketsByStatusChartState(); } class _TicketsByStatusChartState extends ConsumerState { int _touchedIndex = -1; @override Widget build(BuildContext context) { final asyncData = ref.watch(ticketsByStatusReportProvider); return asyncData.when( loading: () => ReportCardWrapper( title: 'Tickets by Status', isLoading: true, height: 220, repaintBoundaryKey: widget.repaintKey, child: const SizedBox.shrink(), ), error: (e, _) => ReportCardWrapper( title: 'Tickets by Status', error: e.toString(), repaintBoundaryKey: widget.repaintKey, child: const SizedBox.shrink(), ), data: (data) { if (data.isEmpty) { return ReportCardWrapper( title: 'Tickets by Status', repaintBoundaryKey: widget.repaintKey, child: const SizedBox( height: 200, child: Center(child: Text('No data for selected period')), ), ); } final total = data.fold(0, (s, e) => s + e.count); return ReportCardWrapper( title: 'Tickets by Status', repaintBoundaryKey: widget.repaintKey, height: 220, child: Row( children: [ Expanded( child: PieChart( PieChartData( pieTouchData: PieTouchData( touchCallback: (FlTouchEvent event, pieTouchResponse) { setState(() { if (!event.isInterestedForInteractions || pieTouchResponse == null || pieTouchResponse.touchedSection == null) { _touchedIndex = -1; return; } _touchedIndex = pieTouchResponse .touchedSection! .touchedSectionIndex; }); }, ), sectionsSpace: 2, centerSpaceRadius: 40, sections: data.asMap().entries.map((entry) { final i = entry.key; final e = entry.value; final isTouched = i == _touchedIndex; return PieChartSectionData( value: e.count.toDouble(), title: '', radius: isTouched ? 60 : 50, color: _ticketStatusColor(context, e.status), borderSide: isTouched ? const BorderSide( color: Colors.white, width: 2, ) : BorderSide.none, ); }).toList(), ), ), ), const SizedBox(width: 12), Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: data.asMap().entries.map((entry) { final i = entry.key; final e = entry.value; final isTouched = i == _touchedIndex; return _LegendItem( color: _ticketStatusColor(context, e.status), label: _capitalize(e.status), value: '${e.count} (${(e.count / total * 100).toStringAsFixed(0)}%)', isTouched: isTouched, ); }).toList(), ), ], ), ); }, ); } Color _ticketStatusColor(BuildContext context, String status) { final colors = Theme.of(context).colorScheme; switch (status) { case 'pending': return colors.tertiary; case 'promoted': return colors.secondary; case 'closed': return colors.primary; default: return colors.outlineVariant; } } } /// Donut chart task counts per status with hover animation. class TasksByStatusChart extends ConsumerStatefulWidget { const TasksByStatusChart({super.key, this.repaintKey}); final GlobalKey? repaintKey; @override ConsumerState createState() => _TasksByStatusChartState(); } class _TasksByStatusChartState extends ConsumerState { int _touchedIndex = -1; @override Widget build(BuildContext context) { final asyncData = ref.watch(tasksByStatusReportProvider); return asyncData.when( loading: () => ReportCardWrapper( title: 'Tasks by Status', isLoading: true, height: 220, repaintBoundaryKey: widget.repaintKey, child: const SizedBox.shrink(), ), error: (e, _) => ReportCardWrapper( title: 'Tasks by Status', error: e.toString(), repaintBoundaryKey: widget.repaintKey, child: const SizedBox.shrink(), ), data: (data) { if (data.isEmpty) { return ReportCardWrapper( title: 'Tasks by Status', repaintBoundaryKey: widget.repaintKey, child: const SizedBox( height: 200, child: Center(child: Text('No data for selected period')), ), ); } final total = data.fold(0, (s, e) => s + e.count); return ReportCardWrapper( title: 'Tasks by Status', repaintBoundaryKey: widget.repaintKey, height: 220, child: Row( children: [ Expanded( child: PieChart( PieChartData( pieTouchData: PieTouchData( touchCallback: (FlTouchEvent event, pieTouchResponse) { setState(() { if (!event.isInterestedForInteractions || pieTouchResponse == null || pieTouchResponse.touchedSection == null) { _touchedIndex = -1; return; } _touchedIndex = pieTouchResponse .touchedSection! .touchedSectionIndex; }); }, ), sectionsSpace: 2, centerSpaceRadius: 40, sections: data.asMap().entries.map((entry) { final i = entry.key; final e = entry.value; final isTouched = i == _touchedIndex; return PieChartSectionData( value: e.count.toDouble(), title: '', radius: isTouched ? 60 : 50, color: _taskStatusColor(context, e.status), borderSide: isTouched ? const BorderSide( color: Colors.white, width: 2, ) : BorderSide.none, ); }).toList(), ), ), ), const SizedBox(width: 12), Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: data.asMap().entries.map((entry) { final i = entry.key; final e = entry.value; final isTouched = i == _touchedIndex; return _LegendItem( color: _taskStatusColor(context, e.status), label: _formatTaskStatus(e.status), value: '${e.count} (${(e.count / total * 100).toStringAsFixed(0)}%)', isTouched: isTouched, ); }).toList(), ), ], ), ); }, ); } Color _taskStatusColor(BuildContext context, String status) { final colors = Theme.of(context).colorScheme; switch (status) { case 'queued': return colors.surfaceContainerHighest; case 'in_progress': return colors.secondary; case 'completed': return colors.primary; case 'cancelled': return colors.error; default: return colors.outlineVariant; } } String _formatTaskStatus(String status) { switch (status) { case 'in_progress': return 'In Progress'; case 'queued': return 'Queued'; case 'completed': return 'Completed'; case 'cancelled': return 'Cancelled'; default: return _capitalize(status); } } } // Shared helpers class _LegendItem extends StatelessWidget { const _LegendItem({ required this.color, required this.label, required this.value, this.isTouched = false, }); final Color color; final String label; final String value; final bool isTouched; @override Widget build(BuildContext context) { final text = Theme.of(context).textTheme; return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( mainAxisSize: MainAxisSize.min, children: [ AnimatedContainer( duration: const Duration(milliseconds: 200), width: isTouched ? 14 : 10, height: isTouched ? 14 : 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 6), AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 200), style: (text.bodySmall ?? const TextStyle()).copyWith( fontWeight: isTouched ? FontWeight.bold : FontWeight.normal, ), child: Text(label), ), const SizedBox(width: 4), AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 200), style: (text.labelSmall ?? const TextStyle()).copyWith( fontWeight: isTouched ? FontWeight.bold : FontWeight.normal, ), child: Text(value), ), ], ), ); } } String _capitalize(String s) => s.isEmpty ? s : '${s[0].toUpperCase()}${s.substring(1)}';