- 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
190 lines
7.7 KiB
Dart
190 lines
7.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../providers/servicio_provider.dart';
|
|
import '../providers/factura_provider.dart';
|
|
import '../providers/configuracion_provider.dart';
|
|
import '../widgets/stat_card.dart';
|
|
import '../utils/currency_utils.dart';
|
|
import 'servicios_screen.dart';
|
|
import 'facturas_screen.dart';
|
|
import 'calendario_screen.dart';
|
|
import 'estadisticas_screen.dart';
|
|
import 'configuracion_screen.dart';
|
|
import 'crear_factura_screen.dart';
|
|
|
|
class DashboardScreen extends StatefulWidget {
|
|
const DashboardScreen({super.key});
|
|
|
|
@override
|
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
|
}
|
|
|
|
class _DashboardScreenState extends State<DashboardScreen> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_cargarDatos();
|
|
}
|
|
|
|
void _cargarDatos() {
|
|
context.read<ServicioProvider>().cargarServicios();
|
|
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';
|
|
|
|
final pendientes = facturaProv.facturas.where((f) => f.estado == 'Pendiente').length;
|
|
final pendienteTotal = facturaProv.facturas
|
|
.where((f) => f.estado == 'Pendiente')
|
|
.fold(0.0, (sum, f) => sum + f.monto);
|
|
final vencidas = facturaProv.facturasVencidas.length;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Factura del Hogar'), centerTitle: true, actions: [
|
|
IconButton(icon: const Icon(Icons.settings), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ConfiguracionScreen()))),
|
|
]),
|
|
body: RefreshIndicator(
|
|
onRefresh: () async => _cargarDatos(),
|
|
child: ListView(
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(child: StatCard(
|
|
title: 'Pendientes',
|
|
value: '$pendientes',
|
|
icon: Icons.pending_actions,
|
|
color: Colors.orange,
|
|
delayMs: 50,
|
|
)),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: StatCard(
|
|
title: 'Vencidas',
|
|
value: '$vencidas',
|
|
icon: Icons.warning_rounded,
|
|
color: Colors.red,
|
|
delayMs: 100,
|
|
)),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(child: StatCard(
|
|
title: 'Pagadas este mes',
|
|
value: facturaProv.facturas.where((f) => f.estado == 'Pagada').length.toString(),
|
|
icon: Icons.check_circle,
|
|
color: Colors.green,
|
|
delayMs: 150,
|
|
)),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: StatCard(
|
|
title: 'Total gastado',
|
|
value: CurrencyUtils.formatPrice(facturaProv.facturas.where((f) => f.estado == 'Pagada').fold(0.0, (sum, f) => sum + f.monto), moneda),
|
|
icon: Icons.attach_money,
|
|
color: Colors.blue,
|
|
delayMs: 200,
|
|
)),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.event_note, size: 20, color: Theme.of(context).colorScheme.primary),
|
|
const SizedBox(width: 8),
|
|
Text('Próximos vencimientos', style: Theme.of(context).textTheme.titleMedium),
|
|
const Spacer(),
|
|
Text(CurrencyUtils.formatPrice(pendienteTotal, moneda),
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
color: Theme.of(context).colorScheme.primary)),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
...facturaProv.facturas
|
|
.where((f) => f.estado == 'Pendiente')
|
|
.take(5)
|
|
.toList()
|
|
.asMap()
|
|
.entries
|
|
.map((entry) {
|
|
final i = entry.key;
|
|
final f = entry.value;
|
|
final servicio = servicioProv.servicios.where((s) => s.id == f.servicioId).firstOrNull;
|
|
return TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0, end: 1),
|
|
duration: Duration(milliseconds: 350 + i * 80),
|
|
curve: Curves.easeOutCubic,
|
|
builder: (context, value, child) {
|
|
return Opacity(
|
|
opacity: value,
|
|
child: Transform.translate(
|
|
offset: Offset(0, 16 * (1 - value)),
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
child: Card(
|
|
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'),
|
|
subtitle: Text('Vence: ${f.fechaVencimiento}'),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(CurrencyUtils.formatPrice(f.monto, moneda),
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
color: Theme.of(context).colorScheme.primary)),
|
|
const SizedBox(width: 4),
|
|
IconButton(
|
|
icon: Icon(Icons.check_circle_outline, color: Theme.of(context).colorScheme.primary),
|
|
onPressed: () => facturaProv.marcarPagada(f.id!),
|
|
tooltip: 'Pagar',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
floatingActionButton: FloatingActionButton.extended(
|
|
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const CrearFacturaScreen())).then((_) => _cargarDatos()),
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('Nueva factura'),
|
|
),
|
|
bottomNavigationBar: NavigationBar(
|
|
selectedIndex: 0,
|
|
onDestinationSelected: (i) {
|
|
switch (i) {
|
|
case 0: break;
|
|
case 1: Navigator.push(context, MaterialPageRoute(builder: (_) => const ServiciosScreen()));
|
|
case 2: Navigator.push(context, MaterialPageRoute(builder: (_) => const FacturasScreen()));
|
|
case 3: Navigator.push(context, MaterialPageRoute(builder: (_) => const CalendarioScreen()));
|
|
case 4: Navigator.push(context, MaterialPageRoute(builder: (_) => const EstadisticasScreen()));
|
|
}
|
|
},
|
|
destinations: const [
|
|
NavigationDestination(icon: Icon(Icons.dashboard), label: 'Dashboard'),
|
|
NavigationDestination(icon: Icon(Icons.electrical_services), label: 'Servicios'),
|
|
NavigationDestination(icon: Icon(Icons.receipt_long), label: 'Facturas'),
|
|
NavigationDestination(icon: Icon(Icons.calendar_month), label: 'Calendario'),
|
|
NavigationDestination(icon: Icon(Icons.bar_chart), label: 'Stats'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|