Facturas recurrentes, edición, padding, y política de privacidad #1

Merged
rafaga21 merged 1 commits from feature/facturas-recurrentes-editar-politica into master 2026-06-15 21:47:16 -04:00
12 changed files with 260 additions and 16 deletions

35
PRIVACY_POLICY.md Normal file
View 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

View File

@@ -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
} }
} }
} }

View File

@@ -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'));

View File

@@ -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,
); );
} }

View File

@@ -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();
} }

View File

@@ -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()

View File

@@ -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'),
), ),
], ],
), ),

View File

@@ -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);

View File

@@ -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),

View File

@@ -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!),

View 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),
],
);
}
}

View File

@@ -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];