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'; /// Dual-series line chart showing tickets and tasks per month. class MonthlyOverviewChart extends ConsumerWidget { const MonthlyOverviewChart({super.key, this.repaintKey}); final GlobalKey? repaintKey; @override Widget build(BuildContext context, WidgetRef ref) { final asyncData = ref.watch(monthlyOverviewReportProvider); return asyncData.when( loading: () => ReportCardWrapper( title: 'Monthly Overview', isLoading: true, height: 280, repaintBoundaryKey: repaintKey, child: const SizedBox.shrink(), ), error: (e, _) => ReportCardWrapper( title: 'Monthly Overview', 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: 'Monthly Overview', repaintBoundaryKey: repaintKey, child: const SizedBox( height: 200, child: Center(child: Text('No data for selected period')), ), ); } final colors = Theme.of(context).colorScheme; final text = Theme.of(context).textTheme; final allCounts = [ ...data.map((e) => e.ticketCount), ...data.map((e) => e.taskCount), ]; final maxY = allCounts.fold(0, (m, e) => e > m ? e : m).toDouble(); return ReportCardWrapper( title: 'Monthly Overview', repaintBoundaryKey: repaintKey, height: 300, child: Column( children: [ // Legend row Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _LegendDot(color: colors.primary, label: 'Tickets'), const SizedBox(width: 16), _LegendDot(color: colors.secondary, label: 'Tasks'), ], ), const SizedBox(height: 12), Expanded( child: LineChart( LineChartData( maxY: maxY * 1.15, minY: 0, lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipItems: (spots) { return spots.map((spot) { final label = spot.barIndex == 0 ? 'Tickets' : 'Tasks'; return LineTooltipItem( '$label: ${spot.y.toInt()}', text.bodySmall!.copyWith( color: colors.onInverseSurface, ), ); }).toList(); }, ), ), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 36, getTitlesWidget: (value, meta) => Text( value.toInt().toString(), style: text.labelSmall, ), ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: 1, getTitlesWidget: (value, meta) { final idx = value.toInt(); if (idx < 0 || idx >= data.length) { return const SizedBox.shrink(); } final month = data[idx].month; // Show abbreviated month: "2026-03" → "Mar" final shortMonth = _shortMonth(month); return Padding( padding: const EdgeInsets.only(top: 6), child: Text(shortMonth, style: text.labelSmall), ); }, ), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), gridData: FlGridData( drawVerticalLine: false, horizontalInterval: maxY > 0 ? (maxY / 4).ceilToDouble() : 1, ), borderData: FlBorderData(show: false), lineBarsData: [ // Tickets line LineChartBarData( spots: List.generate( data.length, (i) => FlSpot(i.toDouble(), data[i].ticketCount.toDouble()), ), isCurved: true, preventCurveOverShooting: true, color: colors.primary, barWidth: 3, dotData: FlDotData(show: data.length <= 12), belowBarData: BarAreaData( show: true, color: colors.primary.withValues(alpha: 0.08), ), ), // Tasks line LineChartBarData( spots: List.generate( data.length, (i) => FlSpot(i.toDouble(), data[i].taskCount.toDouble()), ), isCurved: true, preventCurveOverShooting: true, color: colors.secondary, barWidth: 3, dotData: FlDotData(show: data.length <= 12), belowBarData: BarAreaData( show: true, color: colors.secondary.withValues(alpha: 0.08), ), ), ], ), ), ), ], ), ); } String _shortMonth(String yyyyMm) { const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; final parts = yyyyMm.split('-'); if (parts.length < 2) return yyyyMm; final monthIndex = int.tryParse(parts[1]); if (monthIndex == null || monthIndex < 1 || monthIndex > 12) return yyyyMm; return months[monthIndex - 1]; } } class _LegendDot extends StatelessWidget { const _LegendDot({required this.color, required this.label}); final Color color; final String label; @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 4), Text(label, style: Theme.of(context).textTheme.bodySmall), ], ); } }