import 'package:sysfarma_mobile/src/app/widgets/app_loader.dart'; 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/core/network/api_client.dart'; import 'package:sysfarma_mobile/src/core/storage/tenant_session_store.dart'; import 'package:sysfarma_mobile/src/core/theme/app_tokens.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class PendingVoidedScreen extends ConsumerStatefulWidget { const PendingVoidedScreen({super.key}); @override ConsumerState createState() => _PendingVoidedScreenState(); } class _PendingVoidedScreenState extends ConsumerState { List> _items = const []; bool _isLoading = true; bool _canAnular = true; // Ids con una accion SUNAT en curso (deshabilita los botones de esa fila // y muestra spinner) y ids en fade-out antes de sacarlos de _items. final Set _processingIds = {}; final Set _removingIds = {}; @override void initState() { super.initState(); _loadPermission(); _refresh(); } Future _loadPermission() async { final session = await ref.read(tenantSessionStoreProvider).readSession(); if (!mounted) return; setState(() { _canAnular = (session?.annularSale ?? true) || (session?.isAdmin ?? true); }); } Future _refresh() async { if (mounted) setState(() => _isLoading = true); final response = await ref.read(apiClientProvider).documents( perPage: 50, page: 1, model: 'document', stateTypeId: '13', ); if (!mounted) return; setState(() { _items = response.data ?? const []; _isLoading = false; }); } Future _removeItemAnimated(int id) async { setState(() => _removingIds.add(id)); await Future.delayed(const Duration(milliseconds: 200)); if (!mounted) return; setState(() { _items = _items.where((row) => int.tryParse('${row['id']}') != id).toList(); _removingIds.remove(id); }); } Future _confirm(Map record) async { final id = int.tryParse('${record['id']}') ?? 0; if (id <= 0) return; final go = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Confirmar anulación'), content: const Text( 'Confirma que el comprobante quedará marcado como anulado. Recuerda enviar la comunicación de baja a SUNAT desde la web.'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar'), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Confirmar'), ), ], ), ); if (go != true || !mounted) return; setState(() => _processingIds.add(id)); final response = await ref.read(apiClientProvider).confirmAnularDocument(id: id); if (!mounted) return; setState(() => _processingIds.remove(id)); if (response.success) { await _removeItemAnimated(id); } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(response.message ?? (response.success ? 'OK' : 'Error')), backgroundColor: response.success ? AppTokens.success : AppTokens.danger, ), ); } Future _sendToSunat(Map record) async { final id = int.tryParse('${record['id']}') ?? 0; if (id <= 0) return; final controller = TextEditingController(text: 'Anulación de la operación'); final motivo = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Enviar comunicación de baja'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Indica el motivo de la anulación. Se enviará a SUNAT y el comprobante quedará definitivamente anulado.'), const SizedBox(height: 12), TextField( controller: controller, maxLines: 2, decoration: const InputDecoration( labelText: 'Motivo', border: OutlineInputBorder(), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar'), ), FilledButton.icon( style: FilledButton.styleFrom(backgroundColor: AppTokens.warning), onPressed: () => Navigator.pop(ctx, controller.text.trim()), icon: const Icon(Icons.send_rounded, size: 18), label: const Text('Enviar SUNAT'), ), ], ), ); if (motivo == null || motivo.isEmpty || !mounted) return; setState(() => _processingIds.add(id)); final response = await ref .read(apiClientProvider) .voidedSunatDocument(id: id, description: motivo); if (!mounted) return; setState(() => _processingIds.remove(id)); if (response.success) { await _removeItemAnimated(id); } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(response.message ?? (response.success ? 'OK' : 'Error')), backgroundColor: response.success ? AppTokens.success : AppTokens.danger, ), ); } Future _cancel(Map record) async { final id = int.tryParse('${record['id']}') ?? 0; if (id <= 0) return; final go = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Cancelar anulación'), content: const Text( 'El comprobante volverá a estar activo y el stock se descontará de nuevo. ¿Continuar?'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('No'), ), FilledButton( style: FilledButton.styleFrom(backgroundColor: AppTokens.danger), onPressed: () => Navigator.pop(ctx, true), child: const Text('Sí, revertir'), ), ], ), ); if (go != true || !mounted) return; setState(() => _processingIds.add(id)); final response = await ref.read(apiClientProvider).cancelAnularDocument(id: id); if (!mounted) return; setState(() => _processingIds.remove(id)); if (response.success) { await _removeItemAnimated(id); } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(response.message ?? (response.success ? 'OK' : 'Error')), backgroundColor: response.success ? AppTokens.success : AppTokens.danger, ), ); } @override Widget build(BuildContext context) { return SafeArea( child: RefreshIndicator( onRefresh: _refresh, child: ListView( padding: AppTokens.pagePadding, children: [ AppPanel( title: 'Por anular', subtitle: 'Comprobantes marcados para anular. Confirma para dejarlos como anulados; recuerda enviar la comunicación de baja desde la web.', trailing: Text( '${_items.length}', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: AppTokens.primary, fontWeight: FontWeight.w700, ), ), child: _isLoading ? const Padding( padding: EdgeInsets.symmetric(vertical: 24), child: AppLoader(), ) : _items.isEmpty ? const EmptyStatePanel( icon: Icons.check_circle_outline_rounded, title: 'Sin pendientes', subtitle: 'No hay comprobantes pendientes de anular.', ) : Column( children: [ for (final item in _items) ...[ Builder(builder: (context) { final id = int.tryParse('${item['id']}') ?? 0; final removing = _removingIds.contains(id); return AnimatedOpacity( duration: const Duration(milliseconds: 200), opacity: removing ? 0 : 1, child: AnimatedSize( duration: const Duration(milliseconds: 200), curve: Curves.easeOut, alignment: Alignment.topCenter, child: removing ? const SizedBox.shrink() : _PendingVoidedRow( record: item, canAnular: _canAnular, isProcessing: _processingIds .contains(id), onConfirm: () => _confirm(item), onCancel: () => _cancel(item), onSendSunat: () => _sendToSunat(item), ), ), ); }), if (item != _items.last) const Divider(height: 16), ], ], ), ), ], ), ), ); } } class _PendingVoidedRow extends StatelessWidget { const _PendingVoidedRow({ required this.record, required this.canAnular, required this.isProcessing, required this.onConfirm, required this.onCancel, required this.onSendSunat, }); final Map record; final bool canAnular; final bool isProcessing; final VoidCallback onConfirm; final VoidCallback onCancel; final VoidCallback onSendSunat; static const _spinner = SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ); @override Widget build(BuildContext context) { final number = record['number_full']?.toString() ?? '${record['series'] ?? ''}-${record['number'] ?? ''}'; final customer = record['customer_name']?.toString() ?? record['customer']?['name']?.toString() ?? 'Cliente varios'; final total = record['total']?.toString() ?? '0.00'; final dateOfIssue = record['date_of_issue']?.toString() ?? '-'; return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ CircleAvatar( radius: 18, backgroundColor: AppTokens.warning.withValues(alpha: 0.15), foregroundColor: AppTokens.warning, child: const Icon(Icons.hourglass_top_rounded, size: 18), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( number, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 2), Text( customer, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTokens.secondary, ), ), const SizedBox(height: 2), Text( dateOfIssue, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: AppTokens.secondary, ), ), ], ), ), const SizedBox(width: 8), Text( 'S/ $total', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: AppTokens.textPrimary, fontWeight: FontWeight.w700, ), ), ], ), if (canAnular) ...[ const SizedBox(height: 8), Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: isProcessing ? null : onCancel, style: OutlinedButton.styleFrom( foregroundColor: AppTokens.secondary, side: BorderSide( color: AppTokens.secondary.withValues(alpha: 0.40)), ), icon: isProcessing ? _spinner : const Icon(Icons.undo_rounded, size: 18), label: const Text('Revertir'), ), ), const SizedBox(width: 8), Expanded( child: FilledButton.icon( onPressed: isProcessing ? null : onConfirm, icon: isProcessing ? _spinner : const Icon(Icons.check_rounded, size: 18), label: const Text('Confirmar'), ), ), ], ), ], const SizedBox(height: 6), SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: isProcessing ? null : onSendSunat, style: FilledButton.styleFrom( backgroundColor: AppTokens.warning, foregroundColor: Colors.white, ), icon: isProcessing ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.cloud_upload_rounded, size: 18), label: Text(isProcessing ? 'Enviando...' : 'Enviar SUNAT'), ), ), ], ), ); } }