Files
home-control/lib/screens/crear_factura_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

147 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../models/factura.dart';
import '../providers/factura_provider.dart';
import '../providers/servicio_provider.dart';
import '../providers/configuracion_provider.dart';
import '../utils/currency_utils.dart';
class CrearFacturaScreen extends StatefulWidget {
final Factura? factura;
const CrearFacturaScreen({super.key, this.factura});
@override
State<CrearFacturaScreen> createState() => _CrearFacturaScreenState();
}
class _CrearFacturaScreenState extends State<CrearFacturaScreen> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _montoCtrl;
late TextEditingController _notaCtrl;
int? _servicioId;
DateTime _fechaEmision = DateTime.now();
DateTime _fechaVencimiento = DateTime.now().add(const Duration(days: 30));
String _estado = 'Pendiente';
@override
void initState() {
super.initState();
_montoCtrl = TextEditingController();
_notaCtrl = TextEditingController();
if (widget.factura != null) {
final f = widget.factura!;
_montoCtrl.text = f.monto.toString();
_notaCtrl.text = f.nota ?? '';
_servicioId = f.servicioId;
_fechaEmision = DateTime.parse(f.fechaEmision);
_fechaVencimiento = DateTime.parse(f.fechaVencimiento);
_estado = f.estado;
}
context.read<ServicioProvider>().cargarServicios();
}
@override
void dispose() {
_montoCtrl.dispose();
_notaCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final servicioProv = context.watch<ServicioProvider>();
final facturaProv = context.read<FacturaProvider>();
final configProv = context.watch<ConfiguracionProvider>();
final moneda = configProv.config?.moneda ?? 'USD';
final symbol = CurrencyUtils.getSymbol(moneda);
return Scaffold(
appBar: AppBar(title: Text(widget.factura != null ? 'Editar factura' : 'Nueva factura')),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
DropdownButtonFormField<int>(
value: _servicioId,
decoration: const InputDecoration(labelText: 'Servicio', border: OutlineInputBorder()),
items: servicioProv.servicios.map((s) => DropdownMenuItem(value: s.id, child: Text(s.nombre))).toList(),
onChanged: (v) => setState(() => _servicioId = v),
validator: (v) => v == null ? 'Selecciona un servicio' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _montoCtrl,
decoration: InputDecoration(labelText: 'Monto', border: const OutlineInputBorder(), prefixText: '$symbol '),
keyboardType: TextInputType.numberWithOptions(decimal: true),
validator: (v) {
if (v == null || v.isEmpty) return 'Ingresa el monto';
if (double.tryParse(v) == null) return 'Monto inválido';
return null;
},
),
const SizedBox(height: 16),
ListTile(
title: const Text('Fecha de emisión'),
subtitle: Text(DateFormat('yyyy-MM-dd').format(_fechaEmision)),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final date = await showDatePicker(context: context, firstDate: DateTime(2020), lastDate: DateTime(2035), initialDate: _fechaEmision);
if (date != null) setState(() => _fechaEmision = date);
},
),
ListTile(
title: const Text('Fecha de vencimiento'),
subtitle: Text(DateFormat('yyyy-MM-dd').format(_fechaVencimiento)),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final date = await showDatePicker(context: context, firstDate: DateTime(2020), lastDate: DateTime(2035), initialDate: _fechaVencimiento);
if (date != null) setState(() => _fechaVencimiento = date);
},
),
const SizedBox(height: 16),
TextFormField(
controller: _notaCtrl,
decoration: const InputDecoration(labelText: 'Nota (opcional)', border: OutlineInputBorder()),
maxLines: 2,
),
const SizedBox(height: 16),
SegmentedButton<String>(
segments: const [
ButtonSegment(value: 'Pendiente', label: Text('Pendiente')),
ButtonSegment(value: 'Pagada', label: Text('Pagada')),
],
selected: {_estado},
onSelectionChanged: (v) => setState(() => _estado = v.first),
),
const SizedBox(height: 32),
FilledButton(
onPressed: () async {
if (!_formKey.currentState!.validate()) return;
final factura = Factura(
id: widget.factura?.id,
servicioId: _servicioId!,
monto: double.parse(_montoCtrl.text),
fechaEmision: DateFormat('yyyy-MM-dd').format(_fechaEmision),
fechaVencimiento: DateFormat('yyyy-MM-dd').format(_fechaVencimiento),
estado: _estado,
nota: _notaCtrl.text.isEmpty ? null : _notaCtrl.text,
fechaPago: widget.factura?.fechaPago ?? (_estado == 'Pagada' ? DateFormat('yyyy-MM-dd').format(DateTime.now()) : null),
);
if (widget.factura != null) {
await facturaProv.actualizarFactura(factura);
} else {
await facturaProv.agregarFactura(factura);
}
if (context.mounted) Navigator.pop(context);
},
child: Text(widget.factura != null ? 'Guardar cambios' : 'Guardar factura'),
),
],
),
),
);
}
}