import 'package:sysfarma_mobile/src/app/widgets/app_panel.dart'; import 'package:sysfarma_mobile/src/app/widgets/empty_state_panel.dart'; import 'package:sysfarma_mobile/src/app/widgets/mini_trend_chart.dart'; import 'package:sysfarma_mobile/src/core/network/api_client.dart'; import 'package:sysfarma_mobile/src/core/network/mobile_api_response.dart'; import 'package:sysfarma_mobile/src/core/theme/app_tokens.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:share_plus/share_plus.dart'; enum _ReportPeriod { today, week, month } class GeneralReportScreen extends ConsumerStatefulWidget { const GeneralReportScreen({super.key}); @override ConsumerState createState() => _GeneralReportScreenState(); } class _GeneralReportScreenState extends ConsumerState { _ReportPeriod _period = _ReportPeriod.month; bool _isLoading = true; _ReportBundle? _bundle; DateTime? _lastUpdated; @override void initState() { super.initState(); _refresh(); } ({String period, String? dateStart, String? dateEnd}) _periodParams() { final now = DateTime.now(); String fmt(DateTime d) => '${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; switch (_period) { case _ReportPeriod.today: return (period: 'date', dateStart: fmt(now), dateEnd: fmt(now)); case _ReportPeriod.week: final start = now.subtract(const Duration(days: 6)); return (period: 'date', dateStart: fmt(start), dateEnd: fmt(now)); case _ReportPeriod.month: return (period: 'month', dateStart: null, dateEnd: null); } } Future _refresh() async { if (mounted) setState(() => _isLoading = true); final api = ref.read(apiClientProvider); final params = _periodParams(); try { final results = await Future.wait([ api.salesReport( period: params.period, dateStart: params.dateStart, dateEnd: params.dateEnd), api.inventoryReport(), api.topProductsReport( period: params.period, dateStart: params.dateStart, dateEnd: params.dateEnd, limit: 10), ]); if (!mounted) return; setState(() { _bundle = _ReportBundle( sales: results[0], inventory: results[1], topProducts: results[2], ); _lastUpdated = DateTime.now(); _isLoading = false; }); } catch (e) { if (!mounted) return; setState(() { _bundle = _ReportBundle( sales: MobileApiResponse.failure(message: e.toString()), inventory: MobileApiResponse.failure(message: e.toString()), topProducts: MobileApiResponse.failure(message: e.toString()), ); _isLoading = false; }); } } void _changePeriod(_ReportPeriod period) { if (period == _period) return; setState(() => _period = period); _refresh(); } Future _shareSummary(_ReportBundle bundle) async { final salesData = bundle.sales.data ?? const {}; final salesTotals = Map.from(salesData['totals'] as Map? ?? const {}); final salesPeriod = Map.from(salesData['period'] as Map? ?? const {}); final invData = bundle.inventory.data ?? const {}; final invTotals = Map.from(invData['totals'] as Map? ?? const {}); final lines = [ '📊 *Resumen ${_periodLabel()}*', '${salesPeriod['date_start'] ?? '-'} → ${salesPeriod['date_end'] ?? '-'}', '', '💰 Ventas: ${_fmtCurrency(salesTotals['total'])}', '🧾 Operaciones: ${salesTotals['operations_count'] ?? 0}', '🎯 Ticket promedio: ${_fmtCurrency(salesTotals['avg_ticket'])}', '', '📦 Inventario', ' Costo: ${_fmtCurrency(invTotals['stock_cost_value'])}', ' Venta: ${_fmtCurrency(invTotals['stock_sale_value'])}', ' Utilidad: ${_fmtCurrency(invTotals['potential_profit'])}', ]; await Share.share(lines.join('\n')); } String _periodLabel() { switch (_period) { case _ReportPeriod.today: return 'de hoy'; case _ReportPeriod.week: return 'de los últimos 7 días'; case _ReportPeriod.month: return 'del mes'; } } String _fmtCurrency(Object? value) { if (value == null) return 'S/ 0.00'; final n = double.tryParse(value.toString()) ?? 0.0; final formatted = n.toStringAsFixed(2).replaceAllMapped( RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (m) => '${m[1]},', ); return 'S/ $formatted'; } String _fmtTime(DateTime? dt) { if (dt == null) return ''; final h = dt.hour.toString().padLeft(2, '0'); final m = dt.minute.toString().padLeft(2, '0'); return '$h:$m'; } @override Widget build(BuildContext context) { return SafeArea( child: RefreshIndicator( onRefresh: _refresh, child: ListView( padding: AppTokens.pagePadding, children: [ // Selector de periodo _PeriodSelector( active: _period, onChanged: _changePeriod, ), const SizedBox(height: 12), if (_isLoading) ...[ const SizedBox(height: 24), const Center(child: CircularProgressIndicator()), ] else if (_bundle == null) ...[ const SizedBox(height: 24), const Center(child: Text('Sin datos')), ] else ..._buildBody(_bundle!), ], ), ), ); } List _buildBody(_ReportBundle bundle) { final sales = bundle.sales; final inventory = bundle.inventory; final topProducts = bundle.topProducts; if (!sales.success || !inventory.success) { return [ AppPanel( title: 'Reportes', subtitle: sales.message ?? inventory.message ?? 'No se pudieron cargar los indicadores.', child: FilledButton.icon( onPressed: _refresh, icon: const Icon(Icons.refresh_rounded), label: const Text('Reintentar'), ), ), ]; } final salesData = sales.data ?? const {}; final salesTotals = Map.from(salesData['totals'] as Map? ?? const {}); final salesGraph = Map.from(salesData['graph'] as Map? ?? const {}); final comparison = Map.from(salesData['comparison'] as Map? ?? const {}); final invData = inventory.data ?? const {}; final invTotals = Map.from(invData['totals'] as Map? ?? const {}); final topData = topProducts.data ?? const {}; final topItems = List>.from(topData['items'] as List? ?? const []); final operations = (salesTotals['operations_count'] ?? 0).toString(); final avgTicket = salesTotals['avg_ticket']; final deltaPct = comparison['delta_pct']; final graphLabels = List.from(salesGraph['labels'] as List? ?? const []); final graphSeries = _parseGraphSeries(salesGraph); return [ // Hero compacto Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [AppTokens.primary, AppTokens.primaryDark], ), borderRadius: BorderRadius.circular(AppTokens.radiusMedium), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( 'Ventas ${_periodLabel()}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white.withValues(alpha: 0.85), ), ), ), if (_lastUpdated != null) Text( 'Actualizado ${_fmtTime(_lastUpdated)}', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.white.withValues(alpha: 0.75), ), ), ], ), const SizedBox(height: 4), Text( _fmtCurrency(salesTotals['total']), style: Theme.of(context).textTheme.displaySmall?.copyWith( color: Colors.white, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 6), Wrap( spacing: 8, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: [ if (deltaPct != null) _DeltaChip(deltaPct: (deltaPct as num).toDouble()), Text( '$operations operación(es)', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white.withValues(alpha: 0.85), ), ), if (avgTicket != null) Text( '· Ticket prom. ${_fmtCurrency(avgTicket)}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white.withValues(alpha: 0.85), ), ), ], ), ], ), ), const SizedBox(height: 14), // Tendencia if (graphLabels.isNotEmpty && graphSeries.isNotEmpty) ...[ AppPanel( title: 'Tendencia', child: MiniTrendChart( labels: graphLabels.take(7).toList(), series: graphSeries .map((item) => MiniTrendSeries( name: item.name, values: item.values.take(7).toList(), color: item.color, )) .toList(), ), ), const SizedBox(height: 14), ], // Top productos vendidos AppPanel( title: 'Top productos vendidos', trailing: Text( '${topItems.length}', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: AppTokens.primary, fontWeight: FontWeight.w700, ), ), child: topItems.isEmpty ? const EmptyStatePanel( icon: Icons.shopping_bag_outlined, title: 'Sin ventas', subtitle: 'No hay ventas en el período seleccionado.', ) : Column( children: [ for (var i = 0; i < topItems.length; i++) ...[ _TopProductRow( rank: i + 1, item: topItems[i], formatter: _fmtCurrency, ), if (i < topItems.length - 1) const Divider(height: 16), ], ], ), ), const SizedBox(height: 14), // Inventario consolidado AppPanel( title: 'Inventario', child: Column( children: [ _ReportLine( label: 'Stock total', value: '${invTotals['stock'] ?? 0} uds'), const Divider(height: 22), _ReportLine( label: 'Costo', value: _fmtCurrency(invTotals['stock_cost_value'])), const Divider(height: 22), _ReportLine( label: 'Valor de venta', value: _fmtCurrency(invTotals['stock_sale_value'])), const Divider(height: 22), _ReportLine( label: 'Utilidad potencial', value: _fmtCurrency(invTotals['potential_profit'])), ], ), ), const SizedBox(height: 14), // Compartir SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: () => _shareSummary(bundle), icon: const Icon(Icons.share_rounded), label: const Text('Compartir resumen'), style: FilledButton.styleFrom(backgroundColor: AppTokens.cta), ), ), const SizedBox(height: 24), ]; } List<_ChartSeries> _parseGraphSeries(Map graph) { final datasets = List>.from(graph['datasets'] as List? ?? const []); return datasets.map((dataset) { return _ChartSeries( name: dataset['name']?.toString() ?? dataset['label']?.toString() ?? 'Serie', values: List.from( (dataset['data'] as List? ?? const []) .map((value) => double.tryParse('$value') ?? 0), ), color: _parseColor(dataset['color']?.toString()) ?? AppTokens.primary, ); }).toList(); } Color? _parseColor(String? raw) { if (raw == null || raw.isEmpty) return null; final normalized = raw.replaceAll('#', '').replaceAll('rgb(', '').replaceAll(')', ''); if (normalized.contains(',')) { final parts = normalized .split(',') .map((item) => int.tryParse(item.trim()) ?? 0) .toList(); if (parts.length == 3) { return Color.fromRGBO(parts[0], parts[1], parts[2], 1); } } if (normalized.length == 6) { return Color(int.parse('FF$normalized', radix: 16)); } return null; } } class _PeriodSelector extends StatelessWidget { const _PeriodSelector({required this.active, required this.onChanged}); final _ReportPeriod active; final ValueChanged<_ReportPeriod> onChanged; @override Widget build(BuildContext context) { Widget pill(_ReportPeriod p, String label) { final selected = p == active; return Expanded( child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(AppTokens.radiusSmall), onTap: () => onChanged(p), child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: selected ? AppTokens.primary.withValues(alpha: 0.10) : Colors.transparent, borderRadius: BorderRadius.circular(AppTokens.radiusSmall), ), child: Center( child: Text( label, style: Theme.of(context).textTheme.labelLarge?.copyWith( color: selected ? AppTokens.primary : AppTokens.secondary, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, ), ), ), ), ), ), ); } return Container( decoration: BoxDecoration( color: AppTokens.surface, borderRadius: BorderRadius.circular(AppTokens.radiusSmall), border: Border.all(color: AppTokens.border), ), child: Row( children: [ pill(_ReportPeriod.today, 'Hoy'), pill(_ReportPeriod.week, '7 días'), pill(_ReportPeriod.month, 'Mes'), ], ), ); } } class _DeltaChip extends StatelessWidget { const _DeltaChip({required this.deltaPct}); final double deltaPct; @override Widget build(BuildContext context) { final positive = deltaPct >= 0; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.20), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( positive ? Icons.trending_up_rounded : Icons.trending_down_rounded, size: 14, color: Colors.white, ), const SizedBox(width: 4), Text( '${positive ? '+' : ''}${deltaPct.toStringAsFixed(1)}%', style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w700, ), ), ], ), ); } } class _ReportLine extends StatelessWidget { const _ReportLine({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) { return Row( children: [ Expanded( child: Text( label, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTokens.secondary, ), ), ), Text( value, style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, ), ), ], ); } } class _TopProductRow extends StatelessWidget { const _TopProductRow({ required this.rank, required this.item, required this.formatter, }); final int rank; final Map item; final String Function(Object?) formatter; @override Widget build(BuildContext context) { final qty = item['quantity']?.toString() ?? '0'; final desc = item['description']?.toString() ?? '-'; final total = item['total']; return Row( children: [ SizedBox( width: 22, child: Text( '$rank.', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: AppTokens.secondary, fontWeight: FontWeight.w700, ), ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( desc, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 2), Text( '$qty u', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: AppTokens.secondary, ), ), ], ), ), Text( formatter(total), style: Theme.of(context).textTheme.titleSmall?.copyWith( color: AppTokens.primary, fontWeight: FontWeight.w700, ), ), ], ); } } class _ReportBundle { const _ReportBundle({ required this.sales, required this.inventory, required this.topProducts, }); final MobileApiResponse> sales; final MobileApiResponse> inventory; final MobileApiResponse> topProducts; } class _ChartSeries { const _ChartSeries({ required this.name, required this.values, required this.color, }); final String name; final List values; final Color color; }