import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/reports_provider.dart'; import '../../widgets/app_page_header.dart'; import 'report_date_filter.dart'; import 'report_widget_selector.dart'; import 'report_pdf_export.dart'; import 'widgets/status_charts.dart'; import 'widgets/hourly_charts.dart'; import 'widgets/top_offices_charts.dart'; import 'widgets/top_subjects_charts.dart'; import 'widgets/monthly_overview_chart.dart'; import 'widgets/request_distribution_charts.dart'; import 'widgets/avg_resolution_chart.dart'; import 'widgets/staff_workload_chart.dart'; import 'widgets/conversion_rate_card.dart'; /// Main reports screen — scrollable collection of chart widgets, /// filterable by date range and toggleable per-widget, exportable to PDF. class ReportsScreen extends ConsumerStatefulWidget { const ReportsScreen({super.key}); @override ConsumerState createState() => _ReportsScreenState(); } class _ReportsScreenState extends ConsumerState { // RepaintBoundary keys for PDF capture final _keys = { for (final t in ReportWidgetType.values) t: GlobalKey(), }; bool _exporting = false; Future _exportPdf() async { setState(() => _exporting = true); try { final enabled = ref.read(reportWidgetToggleProvider); final dateRange = ref.read(reportDateRangeProvider); // Wait a frame so charts are rendered before capture await Future.delayed(const Duration(milliseconds: 200)); final captureKeys = {}; for (final type in ReportWidgetType.values) { if (enabled.contains(type)) { captureKeys[type.label] = _keys[type]!; } } final pdfBytes = await ReportPdfExport.generatePdf( captureKeys: captureKeys, dateRange: dateRange, enabledWidgets: enabled, ); await ReportPdfExport.sharePdf(pdfBytes); } catch (e) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('PDF export failed: $e'))); } } finally { if (mounted) setState(() => _exporting = false); } } @override Widget build(BuildContext context) { final enabled = ref.watch(reportWidgetToggleProvider); return Scaffold( body: Column( children: [ // Sticky top section Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1200), child: Column( children: [ AppPageHeader( title: 'Reports', subtitle: 'Analytics and performance insights', actions: [ FilledButton.icon( onPressed: _exporting ? null : _exportPdf, icon: _exporting ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, ), ) : const Icon(Icons.picture_as_pdf, size: 18), label: Text(_exporting ? 'Exporting…' : 'Export PDF'), ), ], ), const SizedBox(height: 8), const ReportDateFilter(), const SizedBox(height: 4), const ReportWidgetSelector(), const SizedBox(height: 8), ], ), ), ), ), // Scrollable chart area Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1200), child: _buildCharts(context, enabled), ), ), ), ), ], ), ); } Widget _buildCharts(BuildContext context, Set enabled) { if (enabled.isEmpty) { return SizedBox( height: 300, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.analytics_outlined, size: 48, color: Theme.of(context).colorScheme.outlineVariant, ), const SizedBox(height: 12), Text( 'No widgets selected', style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 4), Text( 'Use the "Widgets to Include" panel to enable reports.', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ); } return LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth >= 600; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── Status donuts (paired on desktop) ── if (_anyEnabled(enabled, [ ReportWidgetType.ticketsByStatus, ReportWidgetType.tasksByStatus, ])) _responsiveRow( isWide: isWide, children: [ if (enabled.contains(ReportWidgetType.ticketsByStatus)) TicketsByStatusChart( repaintKey: _keys[ReportWidgetType.ticketsByStatus], ), if (enabled.contains(ReportWidgetType.tasksByStatus)) TasksByStatusChart( repaintKey: _keys[ReportWidgetType.tasksByStatus], ), ], ), // ── Conversion rate KPI ── if (enabled.contains(ReportWidgetType.conversionRate)) Padding( padding: const EdgeInsets.only(bottom: 12), child: ConversionRateCard( repaintKey: _keys[ReportWidgetType.conversionRate], ), ), // ── Request type + category donuts ── if (_anyEnabled(enabled, [ ReportWidgetType.requestType, ReportWidgetType.requestCategory, ])) _responsiveRow( isWide: isWide, children: [ if (enabled.contains(ReportWidgetType.requestType)) RequestTypeChart( repaintKey: _keys[ReportWidgetType.requestType], ), if (enabled.contains(ReportWidgetType.requestCategory)) RequestCategoryChart( repaintKey: _keys[ReportWidgetType.requestCategory], ), ], ), // ── Monthly overview (full width) ── if (enabled.contains(ReportWidgetType.monthlyOverview)) Padding( padding: const EdgeInsets.only(bottom: 12), child: MonthlyOverviewChart( repaintKey: _keys[ReportWidgetType.monthlyOverview], ), ), // ── Hourly charts (paired) ── if (_anyEnabled(enabled, [ ReportWidgetType.tasksByHour, ReportWidgetType.ticketsByHour, ])) _responsiveRow( isWide: isWide, children: [ if (enabled.contains(ReportWidgetType.tasksByHour)) TasksByHourChart( repaintKey: _keys[ReportWidgetType.tasksByHour], ), if (enabled.contains(ReportWidgetType.ticketsByHour)) TicketsByHourChart( repaintKey: _keys[ReportWidgetType.ticketsByHour], ), ], ), // ── Top offices + subjects for Tickets (paired) ── if (_anyEnabled(enabled, [ ReportWidgetType.topOfficesTickets, ReportWidgetType.topTicketSubjects, ])) _responsiveRow( isWide: isWide, children: [ if (enabled.contains(ReportWidgetType.topOfficesTickets)) TopOfficesTicketsChart( repaintKey: _keys[ReportWidgetType.topOfficesTickets], ), if (enabled.contains(ReportWidgetType.topTicketSubjects)) TopTicketSubjectsChart( repaintKey: _keys[ReportWidgetType.topTicketSubjects], ), ], ), // ── Top offices + subjects for Tasks (paired) ── if (_anyEnabled(enabled, [ ReportWidgetType.topOfficesTasks, ReportWidgetType.topTaskSubjects, ])) _responsiveRow( isWide: isWide, children: [ if (enabled.contains(ReportWidgetType.topOfficesTasks)) TopOfficesTasksChart( repaintKey: _keys[ReportWidgetType.topOfficesTasks], ), if (enabled.contains(ReportWidgetType.topTaskSubjects)) TopTaskSubjectsChart( repaintKey: _keys[ReportWidgetType.topTaskSubjects], ), ], ), // ── Avg resolution (full width) ── if (enabled.contains(ReportWidgetType.avgResolution)) Padding( padding: const EdgeInsets.only(bottom: 12), child: AvgResolutionChart( repaintKey: _keys[ReportWidgetType.avgResolution], ), ), // ── Staff workload (full width) ── if (enabled.contains(ReportWidgetType.staffWorkload)) Padding( padding: const EdgeInsets.only(bottom: 12), child: StaffWorkloadChart( repaintKey: _keys[ReportWidgetType.staffWorkload], ), ), ], ); }, ); } /// Build either a [Row] (desktop, >=600) or a [Column] (mobile) for a pair /// of chart widgets. Widget _responsiveRow({ required bool isWide, required List children, }) { if (children.isEmpty) return const SizedBox.shrink(); if (isWide && children.length > 1) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: children .expand( (w) => [Expanded(child: w), const SizedBox(width: 12)], ) .toList() ..removeLast(), // remove trailing SizedBox ), ); } // Single column layout return Column( children: children.map((w) { return Padding(padding: const EdgeInsets.only(bottom: 12), child: w); }).toList(), ); } bool _anyEnabled( Set enabled, List types, ) { return types.any(enabled.contains); } }