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'; /// Converts 24h hour int to AM/PM string. String _hourAmPm(int h) { if (h == 0) return '12AM'; if (h < 12) return '${h}AM'; if (h == 12) return '12PM'; return '${h - 12}PM'; } /// Vertical bar chart showing tasks created per hour of day (0–23). class TasksByHourChart extends ConsumerWidget { const TasksByHourChart({super.key, this.repaintKey}); final GlobalKey? repaintKey; @override Widget build(BuildContext context, WidgetRef ref) { final asyncData = ref.watch(tasksByHourReportProvider); return asyncData.when( loading: () => ReportCardWrapper( title: 'Tasks Created by Hour', isLoading: true, height: 260, repaintBoundaryKey: repaintKey, child: const SizedBox.shrink(), ), error: (e, _) => ReportCardWrapper( title: 'Tasks Created by Hour', error: e.toString(), repaintBoundaryKey: repaintKey, child: const SizedBox.shrink(), ), data: (data) => _build(context, data), ); } Widget _build(BuildContext context, List data) { if (data.isEmpty) { return ReportCardWrapper( title: 'Tasks Created by Hour', repaintBoundaryKey: repaintKey, child: const SizedBox( height: 200, child: Center(child: Text('No data for selected period')), ), ); } // Fill all 24 hours final hourMap = {for (final h in data) h.hour: h.count}; final maxCount = data .fold(0, (m, e) => e.count > m ? e.count : m) .toDouble(); final colors = Theme.of(context).colorScheme; return ReportCardWrapper( title: 'Tasks Created by Hour', repaintBoundaryKey: repaintKey, height: 260, child: BarChart( BarChartData( maxY: maxCount * 1.15, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIdx, rod, rodIdx) { return BarTooltipItem( '${_hourAmPm(group.x)} — ${rod.toY.toInt()}', Theme.of(context).textTheme.bodySmall!.copyWith( color: colors.onInverseSurface, ), ); }, ), ), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 36, getTitlesWidget: (value, meta) => Text( value.toInt().toString(), style: Theme.of(context).textTheme.labelSmall, ), ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { final h = value.toInt(); // Show every 3rd hour label to avoid crowding if (h % 3 != 0) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(top: 4), child: Text( _hourAmPm(h), style: Theme.of(context).textTheme.labelSmall, ), ); }, ), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), gridData: FlGridData( drawVerticalLine: false, horizontalInterval: maxCount > 0 ? (maxCount / 4).ceilToDouble() : 1, ), borderData: FlBorderData(show: false), barGroups: List.generate(24, (i) { final count = (hourMap[i] ?? 0).toDouble(); return BarChartGroupData( x: i, barRods: [ BarChartRodData( toY: count, width: 10, borderRadius: const BorderRadius.vertical( top: Radius.circular(4), ), color: colors.primary, ), ], ); }), ), ), ); } } /// Vertical bar chart showing tickets created per hour of day (0–23). class TicketsByHourChart extends ConsumerWidget { const TicketsByHourChart({super.key, this.repaintKey}); final GlobalKey? repaintKey; @override Widget build(BuildContext context, WidgetRef ref) { final asyncData = ref.watch(ticketsByHourReportProvider); return asyncData.when( loading: () => ReportCardWrapper( title: 'Tickets Created by Hour', isLoading: true, height: 260, repaintBoundaryKey: repaintKey, child: const SizedBox.shrink(), ), error: (e, _) => ReportCardWrapper( title: 'Tickets Created by Hour', error: e.toString(), repaintBoundaryKey: repaintKey, child: const SizedBox.shrink(), ), data: (data) => _build(context, data), ); } Widget _build(BuildContext context, List data) { if (data.isEmpty) { return ReportCardWrapper( title: 'Tickets Created by Hour', repaintBoundaryKey: repaintKey, child: const SizedBox( height: 200, child: Center(child: Text('No data for selected period')), ), ); } final hourMap = {for (final h in data) h.hour: h.count}; final maxCount = data .fold(0, (m, e) => e.count > m ? e.count : m) .toDouble(); final colors = Theme.of(context).colorScheme; return ReportCardWrapper( title: 'Tickets Created by Hour', repaintBoundaryKey: repaintKey, height: 260, child: BarChart( BarChartData( maxY: maxCount * 1.15, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIdx, rod, rodIdx) { return BarTooltipItem( '${_hourAmPm(group.x)} — ${rod.toY.toInt()}', Theme.of(context).textTheme.bodySmall!.copyWith( color: colors.onInverseSurface, ), ); }, ), ), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 36, getTitlesWidget: (value, meta) => Text( value.toInt().toString(), style: Theme.of(context).textTheme.labelSmall, ), ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { final h = value.toInt(); if (h % 3 != 0) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(top: 4), child: Text( _hourAmPm(h), style: Theme.of(context).textTheme.labelSmall, ), ); }, ), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), gridData: FlGridData( drawVerticalLine: false, horizontalInterval: maxCount > 0 ? (maxCount / 4).ceilToDouble() : 1, ), borderData: FlBorderData(show: false), barGroups: List.generate(24, (i) { final count = (hourMap[i] ?? 0).toDouble(); return BarChartGroupData( x: i, barRods: [ BarChartRodData( toY: count, width: 10, borderRadius: const BorderRadius.vertical( top: Radius.circular(4), ), color: colors.tertiary, ), ], ); }), ), ), ); } }