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
This commit is contained in:
35
PRIVACY_POLICY.md
Normal file
35
PRIVACY_POLICY.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Privacy Policy — Factura del Hogar
|
||||||
|
|
||||||
|
**Last updated:** June 2026
|
||||||
|
|
||||||
|
## Data Collection
|
||||||
|
|
||||||
|
Factura del Hogar **does not collect, transmit, or share any personal data.** All information you enter (bills, services, amounts, and settings) is stored exclusively on your device using a local SQLite database.
|
||||||
|
|
||||||
|
## Internet Access
|
||||||
|
|
||||||
|
The app **does not require internet access.** The `INTERNET` permission is declared only in debug and profile builds (required by the Flutter framework for development features like hot reload). The release version distributed through Google Play has no internet permission and no network capabilities.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
- **POST_NOTIFICATIONS**: Used to show local reminders about bill due dates.
|
||||||
|
- **RECEIVE_BOOT_COMPLETED**: Used to reschedule notifications after a device restart.
|
||||||
|
- **SCHEDULE_EXACT_ALARM**: Used to deliver bill reminders at the exact scheduled time.
|
||||||
|
|
||||||
|
No permission is used to access the internet, accounts, or external storage.
|
||||||
|
|
||||||
|
## Third-Party Services
|
||||||
|
|
||||||
|
This app uses no third-party analytics, advertising, or tracking services. No data is sent to any server.
|
||||||
|
|
||||||
|
## Data Deletion
|
||||||
|
|
||||||
|
Since all data is stored locally, uninstalling the app removes all associated data. You can also delete individual invoices or services through the app's interface.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
If you have questions about this privacy policy, please open an issue at the project repository.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
© 2026 Factura del Hogar
|
||||||
@@ -5,6 +5,12 @@ plugins {
|
|||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def keystoreProperties = new Properties()
|
||||||
|
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.hogarcontrol.hogarcontrol"
|
namespace = "com.hogarcontrol.hogarcontrol"
|
||||||
compileSdk = 35
|
compileSdk = 35
|
||||||
@@ -28,11 +34,18 @@ android {
|
|||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
storeFile keystoreProperties['storeFile'] ? rootProject.file(keystoreProperties['storeFile']) : null
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
signingConfig = signingConfigs.release
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
|
||||||
signingConfig = signingConfigs.debug
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,20 @@ class DatabaseHelper {
|
|||||||
Future<Database> _initDB(String filePath) async {
|
Future<Database> _initDB(String filePath) async {
|
||||||
final dbPath = await getDatabasesPath();
|
final dbPath = await getDatabasesPath();
|
||||||
final path = join(dbPath, filePath);
|
final path = join(dbPath, filePath);
|
||||||
return await openDatabase(path, version: 1, onCreate: _createDB, onConfigure: _onConfigure);
|
return await openDatabase(path, version: 2, onCreate: _createDB, onUpgrade: _upgradeDB, onConfigure: _onConfigure);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _onConfigure(Database db) async {
|
Future _onConfigure(Database db) async {
|
||||||
await db.execute('PRAGMA foreign_keys = ON');
|
await db.execute('PRAGMA foreign_keys = ON');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future _upgradeDB(Database db, int oldVersion, int newVersion) async {
|
||||||
|
if (oldVersion < 2) {
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE servicios ADD COLUMN es_recurrente INTEGER NOT NULL DEFAULT 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future _createDB(Database db, int version) async {
|
Future _createDB(Database db, int version) async {
|
||||||
await db.execute('''
|
await db.execute('''
|
||||||
CREATE TABLE servicios (
|
CREATE TABLE servicios (
|
||||||
@@ -81,6 +88,13 @@ class DatabaseHelper {
|
|||||||
return await db.delete('servicios', where: 'id = ?', whereArgs: [id]);
|
return await db.delete('servicios', where: 'id = ?', whereArgs: [id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Servicio?> getServicio(int id) async {
|
||||||
|
final db = await database;
|
||||||
|
final maps = await db.query('servicios', where: 'id = ?', whereArgs: [id], limit: 1);
|
||||||
|
if (maps.isEmpty) return null;
|
||||||
|
return Servicio.fromMap(maps.first);
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> insertFactura(Factura factura) async {
|
Future<int> insertFactura(Factura factura) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
return await db.insert('facturas', factura.toMap()..remove('id'));
|
return await db.insert('facturas', factura.toMap()..remove('id'));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class Servicio {
|
|||||||
final int color;
|
final int color;
|
||||||
final int diaVencimiento;
|
final int diaVencimiento;
|
||||||
final String fechaCreacion;
|
final String fechaCreacion;
|
||||||
|
final bool esRecurrente;
|
||||||
|
|
||||||
Servicio({
|
Servicio({
|
||||||
this.id,
|
this.id,
|
||||||
@@ -13,8 +14,28 @@ class Servicio {
|
|||||||
required this.color,
|
required this.color,
|
||||||
required this.diaVencimiento,
|
required this.diaVencimiento,
|
||||||
required this.fechaCreacion,
|
required this.fechaCreacion,
|
||||||
|
this.esRecurrente = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Servicio copyWith({
|
||||||
|
int? id,
|
||||||
|
String? nombre,
|
||||||
|
String? icono,
|
||||||
|
int? color,
|
||||||
|
int? diaVencimiento,
|
||||||
|
String? fechaCreacion,
|
||||||
|
bool? esRecurrente,
|
||||||
|
}) =>
|
||||||
|
Servicio(
|
||||||
|
id: id ?? this.id,
|
||||||
|
nombre: nombre ?? this.nombre,
|
||||||
|
icono: icono ?? this.icono,
|
||||||
|
color: color ?? this.color,
|
||||||
|
diaVencimiento: diaVencimiento ?? this.diaVencimiento,
|
||||||
|
fechaCreacion: fechaCreacion ?? this.fechaCreacion,
|
||||||
|
esRecurrente: esRecurrente ?? this.esRecurrente,
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toMap() => {
|
Map<String, dynamic> toMap() => {
|
||||||
'id': id,
|
'id': id,
|
||||||
'nombre': nombre,
|
'nombre': nombre,
|
||||||
@@ -22,6 +43,7 @@ class Servicio {
|
|||||||
'color': color,
|
'color': color,
|
||||||
'dia_vencimiento': diaVencimiento,
|
'dia_vencimiento': diaVencimiento,
|
||||||
'fecha_creacion': fechaCreacion,
|
'fecha_creacion': fechaCreacion,
|
||||||
|
'es_recurrente': esRecurrente ? 1 : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory Servicio.fromMap(Map<String, dynamic> map) => Servicio(
|
factory Servicio.fromMap(Map<String, dynamic> map) => Servicio(
|
||||||
@@ -31,5 +53,6 @@ class Servicio {
|
|||||||
color: map['color'] as int,
|
color: map['color'] as int,
|
||||||
diaVencimiento: map['dia_vencimiento'] as int,
|
diaVencimiento: map['dia_vencimiento'] as int,
|
||||||
fechaCreacion: map['fecha_creacion'] as String,
|
fechaCreacion: map['fecha_creacion'] as String,
|
||||||
|
esRecurrente: (map['es_recurrente'] as int? ?? 0) == 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import '../database/database_helper.dart';
|
import '../database/database_helper.dart';
|
||||||
import '../models/factura.dart';
|
import '../models/factura.dart';
|
||||||
|
|
||||||
@@ -15,7 +16,11 @@ class FacturaProvider extends ChangeNotifier {
|
|||||||
Future<void> cargarFacturas({String? estado, int? servicioId}) async {
|
Future<void> cargarFacturas({String? estado, int? servicioId}) async {
|
||||||
_cargando = true;
|
_cargando = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
_facturas = await _db.getFacturas(estado: estado, servicioId: servicioId);
|
if (estado == 'vencidas') {
|
||||||
|
_facturas = await _db.getFacturasVencidas();
|
||||||
|
} else {
|
||||||
|
_facturas = await _db.getFacturas(estado: estado, servicioId: servicioId);
|
||||||
|
}
|
||||||
_facturasVencidas = await _db.getFacturasVencidas();
|
_facturasVencidas = await _db.getFacturasVencidas();
|
||||||
_cargando = false;
|
_cargando = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -31,6 +36,39 @@ class FacturaProvider extends ChangeNotifier {
|
|||||||
final hoy = DateTime.now().toIso8601String().split('T')[0];
|
final hoy = DateTime.now().toIso8601String().split('T')[0];
|
||||||
final actualizada = factura.copyWith(estado: 'Pagada', fechaPago: hoy);
|
final actualizada = factura.copyWith(estado: 'Pagada', fechaPago: hoy);
|
||||||
await _db.updateFactura(actualizada);
|
await _db.updateFactura(actualizada);
|
||||||
|
|
||||||
|
final servicio = await _db.getServicio(factura.servicioId);
|
||||||
|
if (servicio != null && servicio.esRecurrente) {
|
||||||
|
final nextEmision = _sumarMes(factura.fechaEmision);
|
||||||
|
final nextVencimiento = _sumarMes(factura.fechaVencimiento);
|
||||||
|
final nueva = Factura(
|
||||||
|
servicioId: factura.servicioId,
|
||||||
|
monto: factura.monto,
|
||||||
|
fechaEmision: nextEmision,
|
||||||
|
fechaVencimiento: nextVencimiento,
|
||||||
|
nota: factura.nota,
|
||||||
|
);
|
||||||
|
await _db.insertFactura(nueva);
|
||||||
|
}
|
||||||
|
|
||||||
|
await cargarFacturas();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _sumarMes(String dateStr) {
|
||||||
|
final date = DateTime.parse(dateStr);
|
||||||
|
final next = DateTime(date.year, date.month + 1, date.day);
|
||||||
|
return DateFormat('yyyy-MM-dd').format(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> marcarPendiente(int id) async {
|
||||||
|
final factura = _facturas.firstWhere((f) => f.id == id);
|
||||||
|
final actualizada = factura.copyWith(estado: 'Pendiente', fechaPago: null);
|
||||||
|
await _db.updateFactura(actualizada);
|
||||||
|
await cargarFacturas();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> actualizarFactura(Factura factura) async {
|
||||||
|
await _db.updateFactura(factura);
|
||||||
await cargarFacturas();
|
await cargarFacturas();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
|||||||
import '../models/configuracion.dart';
|
import '../models/configuracion.dart';
|
||||||
import '../providers/configuracion_provider.dart';
|
import '../providers/configuracion_provider.dart';
|
||||||
import '../utils/currency_utils.dart';
|
import '../utils/currency_utils.dart';
|
||||||
|
import 'privacy_policy_screen.dart';
|
||||||
|
|
||||||
class ConfiguracionScreen extends StatefulWidget {
|
class ConfiguracionScreen extends StatefulWidget {
|
||||||
const ConfiguracionScreen({super.key});
|
const ConfiguracionScreen({super.key});
|
||||||
@@ -64,7 +65,15 @@ class _ConfiguracionScreenState extends State<ConfiguracionScreen> {
|
|||||||
onChanged: (v) => setState(() => _moneda = v!),
|
onChanged: (v) => setState(() => _moneda = v!),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
FilledButton(
|
const SizedBox(height: 16),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.privacy_tip_outlined),
|
||||||
|
title: const Text('Política de privacidad'),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PrivacyPolicyScreen())),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final diasStr = _opcionesDias
|
final diasStr = _opcionesDias
|
||||||
.asMap()
|
.asMap()
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import '../providers/configuracion_provider.dart';
|
|||||||
import '../utils/currency_utils.dart';
|
import '../utils/currency_utils.dart';
|
||||||
|
|
||||||
class CrearFacturaScreen extends StatefulWidget {
|
class CrearFacturaScreen extends StatefulWidget {
|
||||||
const CrearFacturaScreen({super.key});
|
final Factura? factura;
|
||||||
|
const CrearFacturaScreen({super.key, this.factura});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CrearFacturaScreen> createState() => _CrearFacturaScreenState();
|
State<CrearFacturaScreen> createState() => _CrearFacturaScreenState();
|
||||||
@@ -28,6 +29,15 @@ class _CrearFacturaScreenState extends State<CrearFacturaScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_montoCtrl = TextEditingController();
|
_montoCtrl = TextEditingController();
|
||||||
_notaCtrl = 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();
|
context.read<ServicioProvider>().cargarServicios();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +57,7 @@ class _CrearFacturaScreenState extends State<CrearFacturaScreen> {
|
|||||||
final symbol = CurrencyUtils.getSymbol(moneda);
|
final symbol = CurrencyUtils.getSymbol(moneda);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Nueva factura')),
|
appBar: AppBar(title: Text(widget.factura != null ? 'Editar factura' : 'Nueva factura')),
|
||||||
body: Form(
|
body: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@@ -110,18 +120,23 @@ class _CrearFacturaScreenState extends State<CrearFacturaScreen> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (!_formKey.currentState!.validate()) return;
|
if (!_formKey.currentState!.validate()) return;
|
||||||
final factura = Factura(
|
final factura = Factura(
|
||||||
|
id: widget.factura?.id,
|
||||||
servicioId: _servicioId!,
|
servicioId: _servicioId!,
|
||||||
monto: double.parse(_montoCtrl.text),
|
monto: double.parse(_montoCtrl.text),
|
||||||
fechaEmision: DateFormat('yyyy-MM-dd').format(_fechaEmision),
|
fechaEmision: DateFormat('yyyy-MM-dd').format(_fechaEmision),
|
||||||
fechaVencimiento: DateFormat('yyyy-MM-dd').format(_fechaVencimiento),
|
fechaVencimiento: DateFormat('yyyy-MM-dd').format(_fechaVencimiento),
|
||||||
estado: _estado,
|
estado: _estado,
|
||||||
nota: _notaCtrl.text.isEmpty ? null : _notaCtrl.text,
|
nota: _notaCtrl.text.isEmpty ? null : _notaCtrl.text,
|
||||||
fechaPago: _estado == 'Pagada' ? DateFormat('yyyy-MM-dd').format(DateTime.now()) : null,
|
fechaPago: widget.factura?.fechaPago ?? (_estado == 'Pagada' ? DateFormat('yyyy-MM-dd').format(DateTime.now()) : null),
|
||||||
);
|
);
|
||||||
await facturaProv.agregarFactura(factura);
|
if (widget.factura != null) {
|
||||||
|
await facturaProv.actualizarFactura(factura);
|
||||||
|
} else {
|
||||||
|
await facturaProv.agregarFactura(factura);
|
||||||
|
}
|
||||||
if (context.mounted) Navigator.pop(context);
|
if (context.mounted) Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: const Text('Guardar factura'),
|
child: Text(widget.factura != null ? 'Guardar cambios' : 'Guardar factura'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class _CrearServicioScreenState extends State<CrearServicioScreen> {
|
|||||||
String _iconoSeleccionado = 'recibo';
|
String _iconoSeleccionado = 'recibo';
|
||||||
Color _colorSeleccionado = Colors.purple;
|
Color _colorSeleccionado = Colors.purple;
|
||||||
int _diaVencimiento = 15;
|
int _diaVencimiento = 15;
|
||||||
|
bool _esRecurrente = false;
|
||||||
|
|
||||||
final List<Map<String, dynamic>> _iconos = [
|
final List<Map<String, dynamic>> _iconos = [
|
||||||
{'key': 'electricidad', 'icon': Icons.bolt, 'label': 'Electricidad'},
|
{'key': 'electricidad', 'icon': Icons.bolt, 'label': 'Electricidad'},
|
||||||
@@ -46,6 +47,7 @@ class _CrearServicioScreenState extends State<CrearServicioScreen> {
|
|||||||
_iconoSeleccionado = widget.servicio!.icono;
|
_iconoSeleccionado = widget.servicio!.icono;
|
||||||
_colorSeleccionado = Color(widget.servicio!.color);
|
_colorSeleccionado = Color(widget.servicio!.color);
|
||||||
_diaVencimiento = widget.servicio!.diaVencimiento;
|
_diaVencimiento = widget.servicio!.diaVencimiento;
|
||||||
|
_esRecurrente = widget.servicio!.esRecurrente;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +113,13 @@ class _CrearServicioScreenState extends State<CrearServicioScreen> {
|
|||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text('Factura recurrente'),
|
||||||
|
subtitle: const Text('Generar automáticamente la siguiente factura al pagar'),
|
||||||
|
value: _esRecurrente,
|
||||||
|
onChanged: (v) => setState(() => _esRecurrente = v),
|
||||||
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text('Día de vencimiento', style: Theme.of(context).textTheme.titleSmall),
|
Text('Día de vencimiento', style: Theme.of(context).textTheme.titleSmall),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -131,6 +140,7 @@ class _CrearServicioScreenState extends State<CrearServicioScreen> {
|
|||||||
color: _colorSeleccionado.value,
|
color: _colorSeleccionado.value,
|
||||||
diaVencimiento: _diaVencimiento,
|
diaVencimiento: _diaVencimiento,
|
||||||
fechaCreacion: widget.servicio?.fechaCreacion ?? DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
fechaCreacion: widget.servicio?.fechaCreacion ?? DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||||
|
esRecurrente: _esRecurrente,
|
||||||
);
|
);
|
||||||
if (widget.servicio != null) {
|
if (widget.servicio != null) {
|
||||||
await provider.actualizarServicio(servicio);
|
await provider.actualizarServicio(servicio);
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
final moneda = configProv.config?.moneda ?? 'USD';
|
final moneda = configProv.config?.moneda ?? 'USD';
|
||||||
|
|
||||||
final pendientes = facturaProv.facturas.where((f) => f.estado == 'Pendiente').length;
|
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;
|
final vencidas = facturaProv.facturasVencidas.length;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -48,7 +51,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () async => _cargarDatos(),
|
onRefresh: () async => _cargarDatos(),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -95,6 +98,11 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
Icon(Icons.event_note, size: 20, color: Theme.of(context).colorScheme.primary),
|
Icon(Icons.event_note, size: 20, color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('Próximos vencimientos', style: Theme.of(context).textTheme.titleMedium),
|
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),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import '../providers/factura_provider.dart';
|
|||||||
import '../providers/servicio_provider.dart';
|
import '../providers/servicio_provider.dart';
|
||||||
import '../providers/configuracion_provider.dart';
|
import '../providers/configuracion_provider.dart';
|
||||||
import '../utils/currency_utils.dart';
|
import '../utils/currency_utils.dart';
|
||||||
|
import 'crear_factura_screen.dart';
|
||||||
|
|
||||||
class FacturasScreen extends StatefulWidget {
|
class FacturasScreen extends StatefulWidget {
|
||||||
const FacturasScreen({super.key});
|
const FacturasScreen({super.key});
|
||||||
@@ -45,7 +46,7 @@ class _FacturasScreenState extends State<FacturasScreen> {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
FilterChip(label: const Text('Pagadas'), selected: _filtroEstado == 'Pagada', onSelected: (_) => setState(() { _filtroEstado = 'Pagada'; _filtroServicio = null; facturaProv.cargarFacturas(estado: 'Pagada'); })),
|
FilterChip(label: const Text('Pagadas'), selected: _filtroEstado == 'Pagada', onSelected: (_) => setState(() { _filtroEstado = 'Pagada'; _filtroServicio = null; facturaProv.cargarFacturas(estado: 'Pagada'); })),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
FilterChip(label: const Text('Vencidas'), selected: _filtroEstado == 'vencidas', onSelected: (_) => setState(() { _filtroEstado = 'vencidas'; _filtroServicio = null; facturaProv.facturasVencidas; })),
|
FilterChip(label: const Text('Vencidas'), selected: _filtroEstado == 'vencidas', onSelected: (_) => setState(() { _filtroEstado = 'vencidas'; _filtroServicio = null; facturaProv.cargarFacturas(estado: 'vencidas'); })),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -54,7 +55,7 @@ class _FacturasScreenState extends State<FacturasScreen> {
|
|||||||
child: facturaProv.cargando
|
child: facturaProv.cargando
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
|
||||||
itemCount: facturaProv.facturas.length + (facturaProv.facturas.isEmpty ? 1 : 0),
|
itemCount: facturaProv.facturas.length + (facturaProv.facturas.isEmpty ? 1 : 0),
|
||||||
itemBuilder: (_, i) {
|
itemBuilder: (_, i) {
|
||||||
if (facturaProv.facturas.isEmpty) {
|
if (facturaProv.facturas.isEmpty) {
|
||||||
@@ -97,7 +98,13 @@ class _FacturasScreenState extends State<FacturasScreen> {
|
|||||||
),
|
),
|
||||||
title: Text('${servicio?.nombre ?? 'Desconocido'} - ${CurrencyUtils.formatPrice(f.monto, moneda)}',
|
title: Text('${servicio?.nombre ?? 'Desconocido'} - ${CurrencyUtils.formatPrice(f.monto, moneda)}',
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500)),
|
style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
subtitle: Text('Vence: ${f.fechaVencimiento}${f.estado == 'Pagada' ? ' Pagada: ${f.fechaPago ?? ''}' : ''}'),
|
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(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -105,7 +112,17 @@ class _FacturasScreenState extends State<FacturasScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.check_circle, color: vencida ? Colors.red : Colors.green),
|
icon: Icon(Icons.check_circle, color: vencida ? Colors.red : Colors.green),
|
||||||
onPressed: () => facturaProv.marcarPagada(f.id!),
|
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(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||||
onPressed: () => facturaProv.eliminarFactura(f.id!),
|
onPressed: () => facturaProv.eliminarFactura(f.id!),
|
||||||
|
|||||||
62
lib/screens/privacy_policy_screen.dart
Normal file
62
lib/screens/privacy_policy_screen.dart
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class PrivacyPolicyScreen extends StatelessWidget {
|
||||||
|
const PrivacyPolicyScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Política de Privacidad')),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
Text('Factura del Hogar', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_section(context, 'Recolección de datos',
|
||||||
|
'Factura del Hogar no recolecta, transmite ni comparte ningún dato personal. '
|
||||||
|
'Toda la información que ingresas (facturas, servicios, montos y configuración) '
|
||||||
|
'se almacena exclusivamente en tu dispositivo usando una base de datos SQLite local.'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_section(context, 'Acceso a Internet',
|
||||||
|
'La aplicación no requiere acceso a Internet. El permiso INTERNET solo se '
|
||||||
|
'declara en builds de depuración (requerido por Flutter para recarga en caliente). '
|
||||||
|
'La versión de distribución en Google Play no tiene permiso de Internet ni '
|
||||||
|
'capacidades de red.'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_section(context, 'Permisos',
|
||||||
|
'• POST_NOTIFICATIONS: Para mostrar recordatorios locales de vencimiento de facturas.\n'
|
||||||
|
'• RECEIVE_BOOT_COMPLETED: Para reprogramar notificaciones después de reiniciar el dispositivo.\n'
|
||||||
|
'• SCHEDULE_EXACT_ALARM: Para entregar recordatorios a la hora exacta programada.\n\n'
|
||||||
|
'Ningún permiso se usa para acceder a Internet, cuentas o almacenamiento externo.'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_section(context, 'Servicios de terceros',
|
||||||
|
'Esta aplicación no usa servicios de análisis, publicidad ni rastreo de terceros. '
|
||||||
|
'No se envía ningún dato a ningún servidor.'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_section(context, 'Eliminación de datos',
|
||||||
|
'Como todos los datos se almacenan localmente, desinstalar la aplicación elimina '
|
||||||
|
'todos los datos asociados. También puedes eliminar facturas o servicios '
|
||||||
|
'individualmente desde la interfaz de la aplicación.'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_section(context, 'Contacto',
|
||||||
|
'Si tienes preguntas sobre esta política de privacidad, abre un issue en el '
|
||||||
|
'repositorio del proyecto.'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('Última actualización: Junio 2026', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey)),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _section(BuildContext context, String title, String body) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(title, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(body, style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ class ServiciosScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
|
||||||
itemCount: provider.servicios.length,
|
itemCount: provider.servicios.length,
|
||||||
itemBuilder: (_, i) {
|
itemBuilder: (_, i) {
|
||||||
final s = provider.servicios[i];
|
final s = provider.servicios[i];
|
||||||
|
|||||||
Reference in New Issue
Block a user