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 distribution of tasks by request type with hover animation. class RequestTypeChart extends ConsumerStatefulWidget { const RequestTypeChart({super.key, this.repaintKey}); final GlobalKey? repaintKey; static const _typeColors = { 'Install': Color(0xFF4CAF50), 'Repair': Color(0xFFFF9800), 'Upgrade': Color(0xFF2196F3), 'Replace': Color(0xFF9C27B0), 'Other': Color(0xFF607D8B), 'Unspecified': Color(0xFFBDBDBD), }; @override ConsumerState createState() => _RequestTypeChartState(); } class _RequestTypeChartState extends ConsumerState { int _touchedIndex = -1; @override Widget build(BuildContext context) { final asyncData = ref.watch(requestTypeReportProvider); return asyncData.when( loading: () => ReportCardWrapper( title: 'Request Type Distribution', isLoading: true, height: 220, repaintBoundaryKey: widget.repaintKey, child: const SizedBox.shrink(), ), error: (e, _) => ReportCardWrapper( title: 'Request Type Distribution', error: e.toString(), repaintBoundaryKey: widget.repaintKey, child: const SizedBox.shrink(), ), data: (data) => _build(context, data), ); } Widget _build(BuildContext context, List data) { if (data.isEmpty) { return ReportCardWrapper( title: 'Request Type Distribution', 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); final colorScheme = Theme.of(context).colorScheme; final fallbackColors = [ colorScheme.primary, colorScheme.secondary, colorScheme.tertiary, colorScheme.error, colorScheme.outline, ]; Color colorFor(int idx, String name) => RequestTypeChart._typeColors[name] ?? fallbackColors[idx % fallbackColors.length]; return ReportCardWrapper( title: 'Request Type Distribution', 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: colorFor(i, e.name), 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 _HoverLegendItem( color: colorFor(i, e.name), label: e.name, value: '${e.count} (${(e.count / total * 100).toStringAsFixed(0)}%)', isTouched: isTouched, ); }).toList(), ), ], ), ); } } /// Donut chart distribution of tasks by request category with hover animation. class RequestCategoryChart extends ConsumerStatefulWidget { const RequestCategoryChart({super.key, this.repaintKey}); final GlobalKey? repaintKey; static const _catColors = { 'Software': Color(0xFF42A5F5), 'Hardware': Color(0xFFEF5350), 'Network': Color(0xFF66BB6A), 'Unspecified': Color(0xFFBDBDBD), }; @override ConsumerState createState() => _RequestCategoryChartState(); } class _RequestCategoryChartState extends ConsumerState { int _touchedIndex = -1; @override Widget build(BuildContext context) { final asyncData = ref.watch(requestCategoryReportProvider); return asyncData.when( loading: () => ReportCardWrapper( title: 'Request Category Distribution', isLoading: true, height: 220, repaintBoundaryKey: widget.repaintKey, child: const SizedBox.shrink(), ), error: (e, _) => ReportCardWrapper( title: 'Request Category Distribution', error: e.toString(), repaintBoundaryKey: widget.repaintKey, child: const SizedBox.shrink(), ), data: (data) => _build(context, data), ); } Widget _build(BuildContext context, List data) { if (data.isEmpty) { return ReportCardWrapper( title: 'Request Category Distribution', 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); final colorScheme = Theme.of(context).colorScheme; final fallbackColors = [ colorScheme.primary, colorScheme.secondary, colorScheme.tertiary, ]; Color colorFor(int idx, String name) => RequestCategoryChart._catColors[name] ?? fallbackColors[idx % fallbackColors.length]; return ReportCardWrapper( title: 'Request Category Distribution', 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: colorFor(i, e.name), 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 _HoverLegendItem( color: colorFor(i, e.name), label: e.name, value: '${e.count} (${(e.count / total * 100).toStringAsFixed(0)}%)', isTouched: isTouched, ); }).toList(), ), ], ), ); } } // Shared helpers class _HoverLegendItem extends StatelessWidget { const _HoverLegendItem({ 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), ), ], ), ); } }