import 'package:enter_farma_plus_mobile/src/core/network/api_client.dart'; import 'package:enter_farma_plus_mobile/src/core/theme/app_tokens.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; class StocktakeReportScreen extends ConsumerStatefulWidget { const StocktakeReportScreen({super.key, required this.countId}); final int countId; @override ConsumerState createState() => _StocktakeReportScreenState(); } class _StocktakeReportScreenState extends ConsumerState { late Future?> _future; @override void initState() { super.initState(); _future = _load(); } Future?> _load() async { final response = await ref.read(apiClientProvider).physicalCountReport(widget.countId); if (!response.success) return null; return response.data; } void _refresh() => setState(() => _future = _load()); @override Widget build(BuildContext context) { return FutureBuilder?>( future: _future, builder: (context, snapshot) { final report = snapshot.data; final lines = List>.from( report?['lines'] as List? ?? const []); final summary = Map.from(report?['summary'] as Map? ?? const {}); final differences = lines.where((line) { final diff = double.tryParse('${line['difference_qty']}') ?? 0; return diff.abs() > 0.0001 || line['pending_lot'] == true; }).toList(); final okLines = lines.where((line) { final diff = double.tryParse('${line['difference_qty']}') ?? 0; return diff.abs() <= 0.0001 && line['pending_lot'] != true; }).toList(); return Scaffold( appBar: AppBar( title: Text('Reporte #${widget.countId}'), actions: [ IconButton( onPressed: _refresh, icon: const Icon(Icons.refresh_rounded)), ], ), body: RefreshIndicator( onRefresh: () async => _refresh(), child: ListView( padding: AppTokens.pagePadding, children: [ if (snapshot.connectionState == ConnectionState.waiting) const Center(child: CircularProgressIndicator()) else if (report == null) const _ErrorState() else ...[ _SummaryCard(summary: summary), const SizedBox(height: 20), if (differences.isNotEmpty) ...[ _SectionLabel( label: 'Con diferencia', count: differences.length, color: AppTokens.warning, icon: Icons.warning_amber_rounded, ), const SizedBox(height: 8), ...differences.map((line) => _ReportLineCard( countId: widget.countId, line: line, )), const SizedBox(height: 16), ], if (okLines.isNotEmpty) ...[ _SectionLabel( label: 'Sin diferencia', count: okLines.length, color: AppTokens.success, icon: Icons.check_circle_outline_rounded, ), const SizedBox(height: 8), ...okLines.map((line) => _ReportLineCard( countId: widget.countId, line: line, )), ], ], const SizedBox(height: 32), ], ), ), ); }, ); } } // ── Summary card ─────────────────────────────────────────────────────────────── class _SummaryCard extends StatelessWidget { const _SummaryCard({required this.summary}); final Map summary; @override Widget build(BuildContext context) { final totalLines = summary['total_lines'] ?? 0; final okLines = summary['ok_lines'] ?? 0; final diffLines = summary['difference_lines'] ?? 0; final pendingLot = summary['pending_lot_lines'] ?? 0; return Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: Colors.grey.shade200), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Resumen del conteo', style: Theme.of(context) .textTheme .labelLarge ?.copyWith(color: Colors.grey.shade600)), const SizedBox(height: 12), Row( children: [ Expanded( child: _Metric( label: 'Total items', value: '$totalLines', color: AppTokens.primary, )), Expanded( child: _Metric( label: 'Sin dif.', value: '$okLines', color: AppTokens.success, )), Expanded( child: _Metric( label: 'Con dif.', value: '$diffLines', color: diffLines > 0 ? AppTokens.warning : AppTokens.success, )), Expanded( child: _Metric( label: 'Pend. lote', value: '$pendingLot', color: pendingLot > 0 ? AppTokens.danger : AppTokens.secondary, )), ], ), ], ), ), ); } } class _Metric extends StatelessWidget { const _Metric({required this.label, required this.value, required this.color}); final String label; final String value; final Color color; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(value, style: TextStyle( fontSize: 22, fontWeight: FontWeight.w800, color: color)), Text(label, style: TextStyle(fontSize: 11, color: Colors.grey.shade500)), ], ); } } // ── Section label ────────────────────────────────────────────────────────────── class _SectionLabel extends StatelessWidget { const _SectionLabel({ required this.label, required this.count, required this.color, required this.icon, }); final String label; final int count; final Color color; final IconData icon; @override Widget build(BuildContext context) { return Row( children: [ Icon(icon, size: 16, color: color), const SizedBox(width: 6), Text(label, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: color)), const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1), decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), ), child: Text('$count', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: color)), ), ], ); } } // ── Report line card ─────────────────────────────────────────────────────────── class _ReportLineCard extends StatelessWidget { const _ReportLineCard({required this.countId, required this.line}); final int countId; final Map line; @override Widget build(BuildContext context) { final itemId = int.tryParse('${line['item_id']}') ?? 0; final diff = double.tryParse('${line['difference_qty']}') ?? 0; final pendingLot = line['pending_lot'] == true; final lot = line['lot_code']?.toString() ?? ''; final lotDate = line['lot_date_of_due']?.toString() ?? ''; final Color diffColor = pendingLot || diff < -0.0001 ? AppTokens.danger : diff > 0.0001 ? AppTokens.success : AppTokens.secondary; final String diffLabel = pendingLot ? 'Lote pendiente' : diff == 0 ? 'OK' : (diff > 0 ? '+${diff.toStringAsFixed(2)}' : diff.toStringAsFixed(2)); return Card( margin: const EdgeInsets.only(bottom: 8), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: Colors.grey.shade200), ), child: InkWell( borderRadius: BorderRadius.circular(10), onTap: () => context.go('/stocktake/$countId/items/$itemId/kardex'), child: Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( line['description']?.toString() ?? 'Producto', style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 13), ), const SizedBox(height: 3), Row( children: [ Text( 'Contado ${line['counted_qty_base']} / Sistema ${line['system_qty_snapshot']}', style: TextStyle( fontSize: 11, color: Colors.grey.shade500), ), ], ), if (lot.isNotEmpty || lotDate.isNotEmpty) ...[ const SizedBox(height: 2), Text( [ if (lot.isNotEmpty) 'Lote $lot', if (lotDate.isNotEmpty) 'Vence $lotDate', ].join(' · '), style: TextStyle( fontSize: 11, color: Colors.grey.shade500), ), ], ], ), ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3), decoration: BoxDecoration( color: diffColor.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(12), border: Border.all( color: diffColor.withValues(alpha: 0.25)), ), child: Text(diffLabel, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: diffColor)), ), const SizedBox(height: 4), Icon(Icons.chevron_right_rounded, size: 16, color: Colors.grey.shade400), ], ), ], ), ), ), ); } } // ── Error state ──────────────────────────────────────────────────────────────── class _ErrorState extends StatelessWidget { const _ErrorState(); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 48), child: Column( children: [ Icon(Icons.error_outline_rounded, size: 48, color: Colors.grey.shade300), const SizedBox(height: 12), Text('No se pudo cargar el reporte.', style: TextStyle(color: Colors.grey.shade500)), ], ), ); } }