Files
home-control/lib/screens/facturas_screen.dart
Super User bbd20f66da Facturas recurrentes, edición, padding, y política de privacidad
- Servicio: nuevo campo esRecurrente + migración DB v2
- Al marcar pagada una factura recurrente se genera la siguiente
- Editar facturas existentes desde CrearFacturaScreen
- Nuevo método actualizarFactura en FacturaProvider
- padding inferior en listas de facturas y servicios (último item visible)
- Nota de factura visible en subtítulo
- Filtro Vencidas corregido
- Monto pendiente total visible en dashboard
- Botón undo para revertir factura pagada
- Configuración de release signing (keystore)
- Pantalla de política de privacidad en la app + PRIVACY_POLICY.md
2026-06-15 21:45:59 -04:00

143 lines
7.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/factura_provider.dart';
import '../providers/servicio_provider.dart';
import '../providers/configuracion_provider.dart';
import '../utils/currency_utils.dart';
import 'crear_factura_screen.dart';
class FacturasScreen extends StatefulWidget {
const FacturasScreen({super.key});
@override
State<FacturasScreen> createState() => _FacturasScreenState();
}
class _FacturasScreenState extends State<FacturasScreen> {
String? _filtroEstado;
int? _filtroServicio;
@override
void initState() {
super.initState();
context.read<FacturaProvider>().cargarFacturas();
}
@override
Widget build(BuildContext context) {
final facturaProv = context.watch<FacturaProvider>();
final servicioProv = context.watch<ServicioProvider>();
final configProv = context.watch<ConfiguracionProvider>();
final moneda = configProv.config?.moneda ?? 'USD';
return Scaffold(
appBar: AppBar(title: const Text('Facturas')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
FilterChip(label: const Text('Todas'), selected: _filtroEstado == null && _filtroServicio == null, onSelected: (_) => setState(() { _filtroEstado = null; _filtroServicio = null; facturaProv.cargarFacturas(); })),
const SizedBox(width: 8),
FilterChip(label: const Text('Pendientes'), selected: _filtroEstado == 'Pendiente', onSelected: (_) => setState(() { _filtroEstado = 'Pendiente'; _filtroServicio = null; facturaProv.cargarFacturas(estado: 'Pendiente'); })),
const SizedBox(width: 8),
FilterChip(label: const Text('Pagadas'), selected: _filtroEstado == 'Pagada', onSelected: (_) => setState(() { _filtroEstado = 'Pagada'; _filtroServicio = null; facturaProv.cargarFacturas(estado: 'Pagada'); })),
const SizedBox(width: 8),
FilterChip(label: const Text('Vencidas'), selected: _filtroEstado == 'vencidas', onSelected: (_) => setState(() { _filtroEstado = 'vencidas'; _filtroServicio = null; facturaProv.cargarFacturas(estado: 'vencidas'); })),
],
),
),
),
Expanded(
child: facturaProv.cargando
? const Center(child: CircularProgressIndicator())
: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
itemCount: facturaProv.facturas.length + (facturaProv.facturas.isEmpty ? 1 : 0),
itemBuilder: (_, i) {
if (facturaProv.facturas.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(48),
child: Column(
children: [
Icon(Icons.receipt_long, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text('No hay facturas', style: Theme.of(context).textTheme.bodyLarge),
],
),
),
);
}
final f = facturaProv.facturas[i];
final servicio = servicioProv.servicios.where((s) => s.id == f.servicioId).firstOrNull;
final vencida = f.estado == 'Pendiente' && DateTime.tryParse(f.fechaVencimiento)?.isBefore(DateTime.now()) == true;
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: Duration(milliseconds: 300 + i * 50),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 16 * (1 - value)),
child: child,
),
);
},
child: Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Color(servicio?.color ?? 0xFF6200EE),
child: Text(servicio?.nombre.substring(0, 2).toUpperCase() ?? '??',
style: const TextStyle(color: Colors.white, fontSize: 12)),
),
title: Text('${servicio?.nombre ?? 'Desconocido'} - ${CurrencyUtils.formatPrice(f.monto, moneda)}',
style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text.rich(TextSpan(children: [
TextSpan(text: 'Vence: ${f.fechaVencimiento}'),
if (f.estado == 'Pagada' && f.fechaPago != null)
TextSpan(text: ' Pagada: ${f.fechaPago}'),
if (f.nota != null && f.nota!.isNotEmpty)
TextSpan(text: '\n${f.nota}', style: TextStyle(color: Colors.grey[600], fontSize: 12)),
])),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (f.estado == 'Pendiente')
IconButton(
icon: Icon(Icons.check_circle, color: vencida ? Colors.red : Colors.green),
onPressed: () => facturaProv.marcarPagada(f.id!),
)
else
IconButton(
icon: const Icon(Icons.undo, color: Colors.orange),
onPressed: () => facturaProv.marcarPendiente(f.id!),
tooltip: 'Marcar pendiente',
),
IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => CrearFacturaScreen(factura: f))).then((_) => facturaProv.cargarFacturas()),
),
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () => facturaProv.eliminarFactura(f.id!),
),
],
),
),
),
);
},
),
),
],
),
);
}
}