diff --git a/lib/configuraciondomicilio.dart b/lib/configuraciondomicilio.dart index ed2ad28..1449443 100644 --- a/lib/configuraciondomicilio.dart +++ b/lib/configuraciondomicilio.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'notificaciones_service.dart'; class PanelConfiguracionBottomSheet extends StatefulWidget { final Function(String etiqueta, String direccion) onDomicilioGuardado; @@ -125,7 +126,7 @@ class _PanelConfiguracionBottomSheetState extends State _inicioRuta = value); widget.onAlertasChanged(value, 'inicio'); @@ -134,7 +135,7 @@ class _PanelConfiguracionBottomSheetState extends State _aproximacion = value); widget.onAlertasChanged(value, 'aproximacion'); @@ -143,7 +144,7 @@ class _PanelConfiguracionBottomSheetState extends State _retrasosFallas = value); widget.onAlertasChanged(value, 'retrasos'); diff --git a/lib/domicilios.dart b/lib/domicilios.dart index 90e0c91..9aec26d 100644 --- a/lib/domicilios.dart +++ b/lib/domicilios.dart @@ -1,167 +1,94 @@ import 'package:flutter/material.dart'; import 'tarjetaeta.dart'; import 'configuraciondomicilio.dart'; +import 'notificaciones_service.dart'; class Domicilio { final String id; final String etiqueta; final String direccion; - Domicilio({required this.id, required this.etiqueta, required this.direccion}); + Domicilio({ + required this.id, + required this.etiqueta, + required this.direccion, + }); } class GestionDomiciliosScreen extends StatefulWidget { const GestionDomiciliosScreen({super.key}); @override - State createState() => _GestionDomiciliosScreenState(); + State createState() => + _GestionDomiciliosScreenState(); } -class _GestionDomiciliosScreenState extends State { +class _GestionDomiciliosScreenState + extends State { + final List _misDomicilios = [ - Domicilio(id: '1', etiqueta: 'Casa', direccion: 'Av. Paseo de la Reforma 222, CDMX'), - Domicilio(id: '2', etiqueta: 'Trabajo', direccion: 'Colonia Centro, Calle Benito Juárez 45'), + Domicilio( + id: '1', + etiqueta: 'Casa', + direccion: 'Av. Reforma 222, CDMX', + ), + Domicilio( + id: '2', + etiqueta: 'Trabajo', + direccion: 'Benito Juárez 45', + ), ]; - final _formKey = GlobalKey(); - final _direccionController = TextEditingController(); - String _etiquetaSeleccionada = 'Casa'; + bool _notificarInicioRuta = true; + bool _notificarAproximacion = true; + bool _notificarRetrasosFallas = false; - final List _opcionesEtiquetas = ['Casa', 'Trabajo', 'Otro']; - - @override - void dispose() { - _direccionController.dispose(); - super.dispose(); - } - - void _mostrarFormularioAgregar(BuildContext context) { + void _mostrarFormularioAgregar() { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + borderRadius: BorderRadius.vertical( + top: Radius.circular(24), + ), ), builder: (context) { - return StatefulBuilder( - builder: (BuildContext context, StateSetter setModalState) { - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom + 24, - top: 24, - left: 24, - right: 24, - ), - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Registrar Nuevo Domicilio', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - 'Asigna una etiqueta para identificar dónde recogeremos los residuos.', - style: TextStyle(color: Colors.grey[600]), - ), - const SizedBox(height: 20), - - Text('Etiqueta:', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.green[800])), - const SizedBox(height: 8), - Row( - children: _opcionesEtiquetas.map((etiqueta) { - final bool isSelected = _etiquetaSeleccionada == etiqueta; - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: ChoiceChip( - label: Text(etiqueta), - selected: isSelected, - selectedColor: Colors.green[100], - checkmarkColor: Colors.green[800], - labelStyle: TextStyle( - color: isSelected ? Colors.green[800] : Colors.black, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - ), - onSelected: (bool selected) { - setModalState(() { - _etiquetaSeleccionada = etiqueta; - }); - }, - ), - ); - }).toList(), - ), - const SizedBox(height: 20), - - TextFormField( - controller: _direccionController, - decoration: InputDecoration( - labelText: 'Dirección completa', - hintText: 'Calle, número, colonia...', - prefixIcon: const Icon(Icons.location_on, color: Colors.green), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: Colors.green, width: 2), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Por favor, ingresa una dirección'; - } - return null; - }, - ), - const SizedBox(height: 24), - - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[700], - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - onPressed: () { - if (_formKey.currentState!.validate()) { - setState(() { - _misDomicilios.add( - Domicilio( - id: DateTime.now().toString(), - etiqueta: _etiquetaSeleccionada, - direccion: _direccionController.text, - ), - ); - }); - _direccionController.clear(); - Navigator.pop(context); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('¡Domicilio registrado con éxito!'), - backgroundColor: Colors.green, - ), - ); - } - }, - child: const Text('Guardar Domicilio', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - ), - ), - ], + return PanelConfiguracionBottomSheet( + notificarInicioRuta: _notificarInicioRuta, + notificarAproximacion: _notificarAproximacion, + notificarRetrasosFallas: _notificarRetrasosFallas, + onDomicilioGuardado: (String etiqueta, String direccion) { + setState(() { + _misDomicilios.add( + Domicilio( + id: DateTime.now().toString(), + etiqueta: etiqueta, + direccion: direccion, ), + ); + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Domicilio "$etiqueta" agregado'), + backgroundColor: Colors.green, ), ); }, + onAlertasChanged: (bool valor, String tipo) { + setState(() { + switch (tipo) { + case 'inicio': + _notificarInicioRuta = valor; + break; + case 'aproximacion': + _notificarAproximacion = valor; + break; + case 'retrasos': + _notificarRetrasosFallas = valor; + break; + } + }); + }, ); }, ); @@ -170,80 +97,102 @@ class _GestionDomiciliosScreenState extends State { IconData _obtenerIcono(String etiqueta) { switch (etiqueta.toLowerCase()) { case 'casa': - return Icons.home_rounded; + return Icons.home; case 'trabajo': - return Icons.business_center_rounded; + return Icons.business; default: - return Icons.place_rounded; + return Icons.location_on; } } @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.grey[100], + appBar: AppBar( - title: const Text('Mis Domicilios', style: TextStyle(fontWeight: FontWeight.bold)), - backgroundColor: Colors.green[700], + title: const Text('Mis Domicilios'), + backgroundColor: Colors.green, foregroundColor: Colors.white, + + actions: [ + IconButton( + icon: const Icon(Icons.notifications_outlined), + tooltip: 'Alertas y Notificaciones', + onPressed: () async { + final resultado = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AlertasNotificacionesScreen( + notificarInicioRuta: _notificarInicioRuta, + notificarAproximacion: _notificarAproximacion, + notificarRetrasosFallas: _notificarRetrasosFallas, + ), + ), + ); + + if (resultado != null) { + setState(() { + _notificarInicioRuta = resultado['inicio']; + _notificarAproximacion = resultado['aproximacion']; + _notificarRetrasosFallas = resultado['retrasos']; + }); + } + }, + ), + ], ), + body: Column( children: [ - // USAMOS EL WIDGET EXTERNO AQUÍ DIRECTAMENTE + + // ← TARJETA ETA corregida const Padding( - padding: EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0), - child: TarjetaEtaWidget( - horaInicio: '7:20 p.m.', - horaFin: '7:35 p.m.', - minutosRestantes: 15, - estadoCamion: 'en_camino', - ), + padding: EdgeInsets.all(16), + child: TarjetaEtaWidget(), // ← child bien cerrado aquí ), const Padding( - padding: EdgeInsets.only(left: 18.0, top: 16.0, bottom: 4.0), + padding: EdgeInsets.symmetric(horizontal: 16), child: Align( alignment: Alignment.centerLeft, child: Text( - 'Lugares de Recolección', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey), + 'Lugares Registrados', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), ), ), + const SizedBox(height: 10), + Expanded( - child: _misDomicilios.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.no_accounts_rounded, size: 80, color: Colors.grey[400]), - const SizedBox(height: 16), - Text('Aún no tienes domicilios registrados', style: TextStyle(color: Colors.grey[600], fontSize: 16)), - ], - ), - ) - : ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: ListView.builder( itemCount: _misDomicilios.length, itemBuilder: (context, index) { final domicilio = _misDomicilios[index]; - return Card( - margin: const EdgeInsets.only(bottom: 12), - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), child: ListTile( leading: CircleAvatar( - backgroundColor: Colors.green[50], - child: Icon(_obtenerIcono(domicilio.etiqueta), color: Colors.green[700]), + backgroundColor: Colors.green[100], + child: Icon( + _obtenerIcono(domicilio.etiqueta), + color: Colors.green, + ), ), - title: Text( - domicilio.etiqueta, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - subtitle: Text(domicilio.direccion, maxLines: 2, overflow: TextOverflow.ellipsis), + title: Text(domicilio.etiqueta), + subtitle: Text(domicilio.direccion), trailing: IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.redAccent), + icon: const Icon(Icons.delete, color: Colors.red), onPressed: () { setState(() { _misDomicilios.removeAt(index); @@ -257,12 +206,13 @@ class _GestionDomiciliosScreenState extends State { ), ], ), + floatingActionButton: FloatingActionButton.extended( - onPressed: () => _mostrarFormularioAgregar(context), - backgroundColor: Colors.green[700], + onPressed: _mostrarFormularioAgregar, + backgroundColor: Colors.green, foregroundColor: Colors.white, - icon: const Icon(Icons.add_location_alt_rounded), - label: const Text('Agregar lugar'), + icon: const Icon(Icons.add), + label: const Text('Agregar'), ), ); } diff --git a/lib/gestion_domicilios.dart b/lib/gestion_domicilios.dart index 4b00507..158e5e2 100644 --- a/lib/gestion_domicilios.dart +++ b/lib/gestion_domicilios.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -// Asegúrate de que el nombre del import coincida exactamente con tu archivo creado import 'panel_configuracion_bottom_sheet.dart'; +import 'tarjetaeta.dart'; // Importamos el widget externo estilizado // ========================================== // 1. MODELO DE DATOS @@ -14,59 +14,7 @@ class Domicilio { } // ========================================== -// 2. COMPONENTE: TARJETA DE LLEGADA (ETA) -// ========================================== -class TarjetaEtaWidget extends StatelessWidget { - final String horaInicio; - final String horaFin; - final int minutosRestantes; - final String estadoCamion; - - const TarjetaEtaWidget({ - super.key, - required this.horaInicio, - required this.horaFin, - required this.minutosRestantes, - required this.estadoCamion, - }); - - @override - Widget build(BuildContext context) { - final Color colorTema = estadoCamion == 'retrasado' ? Colors.amber[700]! : Colors.green[700]!; - - return Card( - elevation: 3, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.local_shipping_rounded, color: colorTema), - const SizedBox(width: 8), - Text( - estadoCamion == 'retrasado' ? 'Estado: Demorado' : 'Ruta en Progreso', - style: TextStyle(color: colorTema, fontWeight: FontWeight.bold), - ), - ], - ), - const SizedBox(height: 8), - Text( - 'Llega en aproximadamente $minutosRestantes minutos', - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - Text('Entre las $horaInicio y las $horaFin.'), - ], - ), - ), - ); - } -} - -// ========================================== -// 3. PANTALLA PRINCIPAL +// 2. PANTALLA PRINCIPAL // ========================================== class GestionDomiciliosScreen extends StatefulWidget { const GestionDomiciliosScreen({super.key}); @@ -85,7 +33,7 @@ class _GestionDomiciliosScreenState extends State { bool _notificarAproximacion = true; bool _notificarRetrasosFallas = true; - void _simularNotificacionPush({required String titulo, required String mensaje, required IconData icono, required Color color}) { + /*void _simularNotificacionPush({required String titulo, required String mensaje, required IconData icono, required Color color}) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -100,7 +48,7 @@ class _GestionDomiciliosScreenState extends State { ), ), ); - } + }*/ void _abrirAjustesYFormulario(BuildContext context) { showModalBottomSheet( @@ -110,8 +58,6 @@ class _GestionDomiciliosScreenState extends State { borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (context) { - // Aquí llamamos a la clase externa. Al limpiar el archivo, - // Android Studio sabrá perfectamente que nos referimos al Widget. return PanelConfiguracionBottomSheet( notificarInicioRuta: _notificarInicioRuta, notificarAproximacion: _notificarAproximacion, @@ -147,15 +93,10 @@ class _GestionDomiciliosScreenState extends State { ), body: Column( children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: TarjetaEtaWidget( - horaInicio: '7:20 p.m.', - horaFin: '7:35 p.m.', - minutosRestantes: 15, - estadoCamion: 'en_camino', - ), + const Padding( + padding: EdgeInsets.all(16.0), ), + const TarjetaEtaWidget(), Expanded( child: ListView.builder( itemCount: _misDomicilios.length, diff --git a/lib/login.dart b/lib/login.dart index f6ea036..6cb4b4b 100644 --- a/lib/login.dart +++ b/lib/login.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'domicilios.dart'; // Para navegar aquí tras el login +import 'domicilios.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -8,45 +8,35 @@ class LoginScreen extends StatefulWidget { State createState() => _LoginScreenState(); } -class _LoginScreenState extends State with SingleTickerProviderStateMixin { +class _LoginScreenState extends State { final _formKey = GlobalKey(); - // Controladores para capturar el texto - final _usuarioController = TextEditingController(); - final _passwordController = TextEditingController(); + final TextEditingController emailController = + TextEditingController(); - bool _obscurePassword = true; // Ocultar/mostrar contraseña - late TabController _tabController; // Controla si inicia con Email o Teléfono + final TextEditingController passwordController = + TextEditingController(); - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this); - } + bool ocultarPassword = true; - @override - void dispose() { - _usuarioController.dispose(); - _passwordController.dispose(); - _tabController.dispose(); - super.dispose(); - } - - void _iniciarSesion() { + void iniciarSesion() { if (_formKey.currentState!.validate()) { - // Aquí irá tu lógica futura con Firebase/Supabase + // Simulación de login exitoso ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('¡Ingreso exitoso!'), + content: Text('Inicio de sesión exitoso'), backgroundColor: Colors.green, ), ); - // Navegamos directamente a tu pantalla de Gestión de Domicilios + // Navegar a la pantalla principal Navigator.pushReplacement( context, - MaterialPageRoute(builder: (context) => const GestionDomiciliosScreen()), + MaterialPageRoute( + builder: (context) => + const GestionDomiciliosScreen(), + ), ); } } @@ -55,165 +45,180 @@ class _LoginScreenState extends State with SingleTickerProviderStat Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - body: SafeArea( + + body: Center( child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), + padding: const EdgeInsets.all(25), + child: Form( key: _formKey, + child: Column( - crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ - const SizedBox(height: 40), - // Icono e Identidad de la App + // Logo CircleAvatar( - radius: 45, - backgroundColor: Colors.green[50], - child: Icon(Icons.recycling_rounded, size: 55, color: Colors.green[700]), - ), - const SizedBox(height: 16), - Text( - 'EcoRecolección', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.green[800], + radius: 50, + backgroundColor: Colors.green.shade100, + + child: Icon( + Icons.recycling, + size: 60, + color: Colors.green.shade700, ), ), - Text( - 'Por una comunidad más limpia y educada', - style: TextStyle(color: Colors.grey[600], fontSize: 14), + + const SizedBox(height: 20), + + const Text( + 'EcoRecolección', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + ), ), + + const SizedBox(height: 10), + + Text( + 'Inicia sesión para continuar', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 16, + ), + ), + const SizedBox(height: 40), - // Selector de tipo de ingreso (Email / Teléfono) - Container( - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(12), - ), - child: TabBar( - controller: _tabController, - indicatorSize: TabBarIndicatorSize.tab, - dividerColor: Colors.transparent, - indicator: BoxDecoration( - color: Colors.green[700], - borderRadius: BorderRadius.circular(12), - ), - labelColor: Colors.white, - unselectedLabelColor: Colors.grey[600], - labelStyle: const TextStyle(fontWeight: FontWeight.bold), - tabs: const [ - Tab(text: 'Correo Electrónico'), - Tab(text: 'Teléfono'), - ], - onTap: (index) { - _usuarioController.clear(); // Limpia al cambiar de pestaña - }, - ), - ), - const SizedBox(height: 24), - - // Contenedor dinámico según el Tab activo - AnimatedBuilder( - animation: _tabController, - builder: (context, child) { - bool esEmail = _tabController.index == 0; - return TextFormField( - controller: _usuarioController, - keyboardType: esEmail ? TextInputType.emailAddress : TextInputType.phone, - decoration: InputDecoration( - labelText: esEmail ? 'Correo Electrónico' : 'Número de Teléfono', - hintText: esEmail ? 'ejemplo@correo.com' : '10 dígitos', - prefixIcon: Icon(esEmail ? Icons.email_outlined : Icons.phone_android_rounded, color: Colors.green[700]), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.green[700]!, width: 2), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return esEmail ? 'Ingresa tu correo' : 'Ingresa tu teléfono'; - } - if (esEmail && !RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { - return 'Introduce un correo válido'; - } - return null; - }, - ); - }, - ), - const SizedBox(height: 16), - - // Campo de Contraseña + // Campo correo TextFormField( - controller: _passwordController, - obscureText: _obscurePassword, + controller: emailController, + decoration: InputDecoration( - labelText: 'Contraseña', - prefixIcon: Icon(Icons.lock_outline_rounded, color: Colors.green[700]), - suffixIcon: IconButton( - icon: Icon(_obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: Colors.grey), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.green[700]!, width: 2), + labelText: 'Correo Electrónico', + hintText: 'ejemplo@correo.com', + + prefixIcon: const Icon(Icons.email), + + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(15), ), ), + validator: (value) { if (value == null || value.isEmpty) { - return 'Ingresa tu contraseña'; + return 'Ingresa tu correo'; } - if (value.length < 6) { - return 'Debe tener al menos 6 caracteres'; + + if (!value.contains('@')) { + return 'Correo inválido'; } + return null; }, ), - // Olvidé mi contraseña - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () {}, - child: Text('¿Olvidaste tu contraseña?', style: TextStyle(color: Colors.green[800], fontWeight: FontWeight.w600)), - ), - ), - const SizedBox(height: 16), + const SizedBox(height: 20), - // Botón Ingresar + // Campo contraseña + TextFormField( + controller: passwordController, + obscureText: ocultarPassword, + + decoration: InputDecoration( + labelText: 'Contraseña', + + prefixIcon: + const Icon(Icons.lock), + + suffixIcon: IconButton( + icon: Icon( + ocultarPassword + ? Icons.visibility_off + : Icons.visibility, + ), + + onPressed: () { + setState(() { + ocultarPassword = + !ocultarPassword; + }); + }, + ), + + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(15), + ), + ), + + validator: (value) { + if (value == null || value.isEmpty) { + return 'Ingresa tu contraseña'; + } + + if (value.length < 6) { + return 'Mínimo 6 caracteres'; + } + + return null; + }, + ), + + const SizedBox(height: 30), + + // Botón iniciar sesión SizedBox( width: double.infinity, - height: 52, + height: 55, + child: ElevatedButton( + onPressed: iniciarSesion, + style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[700], + backgroundColor: Colors.green, foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - elevation: 1, + + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(15), + ), + ), + + child: const Text( + 'Iniciar Sesión', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), - onPressed: _iniciarSesion, - child: const Text('Iniciar Sesión', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ), ), - const SizedBox(height: 24), - // Crear cuenta nueva + const SizedBox(height: 20), + + // Registro Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ - Text('¿No tienes una cuenta? ', style: TextStyle(color: Colors.grey[600])), - GestureDetector( - onTap: () {}, // Aquí abrirías la pantalla de registro - child: Text( - 'Regístrate', - style: TextStyle(color: Colors.green[800], fontWeight: FontWeight.bold, decoration: TextDecoration.underline), + const Text( + '¿No tienes cuenta?', + ), + + TextButton( + onPressed: () {}, + + child: const Text( + 'Registrarse', + style: TextStyle( + color: Colors.green, + fontWeight: FontWeight.bold, + ), ), ), ], diff --git a/lib/main.dart b/lib/main.dart index eb1c5bc..fef2699 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; -import 'login.dart'; // Importamos la nueva pantalla de Login +import 'login.dart'; +import 'notificaciones_service.dart'; // ← agrega este import -void main() { - runApp(const MyApp()); +void main() async { + WidgetsFlutterBinding.ensureInitialized(); // ← primero esto + await initNotifications(); // ← luego esto + runApp(const MyApp()); // ← al final esto } class MyApp extends StatelessWidget { @@ -11,7 +14,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'EcoRecolección App', + title: 'Reco App', debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed( @@ -20,7 +23,6 @@ class MyApp extends StatelessWidget { ), useMaterial3: true, ), - // Configuramos la app para que arranque directo en el Login home: const LoginScreen(), ); } diff --git a/lib/notificaciones_service.dart b/lib/notificaciones_service.dart index 6448c3d..745000e 100644 --- a/lib/notificaciones_service.dart +++ b/lib/notificaciones_service.dart @@ -1,53 +1,406 @@ +import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'camion_estado.dart'; -class NotificacionesService { - // Instancia única (Singleton) - static final NotificacionesService _instance = NotificacionesService._internal(); - factory NotificacionesService() => _instance; - NotificacionesService._internal(); +// ─── INSTANCIA GLOBAL ─────────────────────────────────────────── +final FlutterLocalNotificationsPlugin notificationsPlugin = +FlutterLocalNotificationsPlugin(); - final FlutterLocalNotificationsPlugin _localNotificationsPlugin = FlutterLocalNotificationsPlugin(); +// ─── INICIALIZAR ──────────────────────────────────────────────── +Future initNotifications() async { + const AndroidInitializationSettings androidSettings = + AndroidInitializationSettings('@mipmap/ic_launcher'); - // Inicializa el sistema de alertas del teléfono - Future inicializarNotificaciones() async { - const AndroidInitializationSettings androidSettings = - AndroidInitializationSettings('@mipmap/ic_launcher'); // Icono por defecto de tu app Android + const InitializationSettings settings = + InitializationSettings(android: androidSettings); - const InitializationSettings initSettings = InitializationSettings( - android: androidSettings, - ); + await notificationsPlugin.initialize(settings); +} - // SOLUCIÓN: En las versiones actuales se debe pasar como argumento con nombre 'settings:' - await _localNotificationsPlugin.initialize( - initSettings, - // Si en un futuro necesitas reaccionar cuando el usuario presiona la notificación: - // onDidReceiveNotificationResponse: (NotificationResponse response) { ... } - ); +// ─── ENVIAR NOTIFICACIÓN ──────────────────────────────────────── +Future enviarNotificacion({ + required int id, + required String titulo, + required String cuerpo, +}) async { + const AndroidNotificationDetails androidDetails = AndroidNotificationDetails( + 'canal_camion', + 'Camión Recolector', + channelDescription: 'Notificaciones del camión recolector', + importance: Importance.max, + priority: Priority.high, + icon: '@mipmap/ic_launcher', + ); + + const NotificationDetails details = + NotificationDetails(android: androidDetails); + + await notificationsPlugin.show(id, titulo, cuerpo, details); +} + +// ════════════════════════════════════════════════════════════════ +// PANTALLA +// ════════════════════════════════════════════════════════════════ +class AlertasNotificacionesScreen extends StatefulWidget { + final bool notificarInicioRuta; + final bool notificarAproximacion; + final bool notificarRetrasosFallas; + + const AlertasNotificacionesScreen({ + super.key, + required this.notificarInicioRuta, + required this.notificarAproximacion, + required this.notificarRetrasosFallas, + }); + + @override + State createState() => + _AlertasNotificacionesScreenState(); +} + +class _AlertasNotificacionesScreenState + extends State { + + late bool notificarInicioRuta; + late bool notificarCamionCerca; + late bool notificarRetrasos; + bool privacidadActiva = true; + + @override + void initState() { + super.initState(); + notificarInicioRuta = widget.notificarInicioRuta; + notificarCamionCerca = widget.notificarAproximacion; + notificarRetrasos = widget.notificarRetrasosFallas; + + // Sincroniza switches con el estado global + camionEstado.notificarInicioRuta = notificarInicioRuta; + camionEstado.notificarCamionCerca = notificarCamionCerca; + camionEstado.notificarRetrasos = notificarRetrasos; + + // Escucha cambios del camión para actualizar UI + camionEstado.addListener(_actualizar); } - // Método genérico para disparar la alerta nativa en Android/iOS - Future mostrarNotificacionPush({ - required int id, - required String titulo, - required String mensaje, - }) async { - const AndroidNotificationDetails androidDetails = AndroidNotificationDetails( - 'canal_alertas_operativas', // ID del canal - 'Alertas Operativas Camión', // Nombre del canal visible para el usuario - channelDescription: 'Avisos de rutas, aproximaciones e imprevistos mecánicos', - importance: Importance.max, - priority: Priority.high, - playSound: true, - ); - const NotificationDetails platformDetails = NotificationDetails( - android: androidDetails, - ); + void _actualizar() { + if (mounted) setState(() {}); + } - await _localNotificationsPlugin.show( - id, - titulo, - mensaje, - platformDetails, + @override + void dispose() { + camionEstado.removeListener(_actualizar); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[100], + + appBar: AppBar( + title: const Text('Alertas y Notificaciones'), + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + + // ── TARJETA SIMULADOR ───────────────────────────── + Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + const Row( + children: [ + Icon(Icons.local_shipping, color: Colors.green, size: 30), + SizedBox(width: 10), + Text( + 'Estado del Camión', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Barra de progreso + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: LinearProgressIndicator( + value: camionEstado.positionId / 8, + minHeight: 12, + backgroundColor: Colors.green[100], + valueColor: const AlwaysStoppedAnimation( + Colors.green, + ), + ), + ), + + const SizedBox(height: 12), + + // Etapa actual + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.green[50], + borderRadius: BorderRadius.circular(12), + ), + child: Text( + CamionEstado.etapas[camionEstado.positionId] ?? '', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + const SizedBox(height: 8), + + Text( + 'Posición: ${camionEstado.positionId} / 8', + style: TextStyle(color: Colors.grey[600], fontSize: 13), + ), + + const SizedBox(height: 16), + + // Botones — usan el timer global + Row( + children: [ + + Expanded( + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: camionEstado.corriendo + ? Colors.grey + : Colors.green, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: camionEstado.corriendo + ? null + : camionEstado.iniciarTimer, + icon: const Icon(Icons.play_arrow), + label: const Text('Iniciar'), + ), + ), + + const SizedBox(width: 12), + + Expanded( + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.green, + side: const BorderSide(color: Colors.green), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: camionEstado.reiniciar, + icon: const Icon(Icons.refresh), + label: const Text('Reiniciar'), + ), + ), + ], + ), + ], + ), + ), + ), + + const SizedBox(height: 20), + + // ── TARJETA ALERTAS ─────────────────────────────── + Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + const Row( + children: [ + Icon(Icons.notifications_active, + color: Colors.green, size: 30), + SizedBox(width: 10), + Text( + 'Configuración de Alertas', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + + const SizedBox(height: 20), + + SwitchListTile( + value: notificarInicioRuta, + activeColor: Colors.green, + title: const Text('Inicio de Ruta'), + subtitle: const Text( + 'Recibir aviso cuando el camión inicie su recorrido.', + ), + secondary: const Icon(Icons.route), + onChanged: (value) { + setState(() => notificarInicioRuta = value); + camionEstado.notificarInicioRuta = value; + }, + ), + + const Divider(), + + SwitchListTile( + value: notificarCamionCerca, + activeColor: Colors.green, + title: const Text('Camión Cercano'), + subtitle: const Text( + 'Notificación cuando el camión esté próximo a tu domicilio.', + ), + secondary: const Icon(Icons.local_shipping), + onChanged: (value) { + setState(() => notificarCamionCerca = value); + camionEstado.notificarCamionCerca = value; + }, + ), + + const Divider(), + + SwitchListTile( + value: notificarRetrasos, + activeColor: Colors.green, + title: const Text('Retrasos o Fallas'), + subtitle: const Text( + 'Avisos por tráfico, retrasos o fallas mecánicas.', + ), + secondary: const Icon(Icons.warning_amber_rounded), + onChanged: (value) { + setState(() => notificarRetrasos = value); + camionEstado.notificarRetrasos = value; + }, + ), + ], + ), + ), + ), + + const SizedBox(height: 20), + + // ── TARJETA PRIVACIDAD ──────────────────────────── + Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + const Row( + children: [ + Icon(Icons.security, color: Colors.redAccent, size: 30), + SizedBox(width: 10), + Text( + 'Privacidad y Seguridad', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + + const SizedBox(height: 20), + + SwitchListTile( + value: privacidadActiva, + activeColor: Colors.green, + title: const Text('Protección Anti-Snooping'), + subtitle: const Text( + 'Impide explorar rutas, domicilios o información de otros usuarios.', + ), + secondary: const Icon(Icons.lock_outline), + onChanged: (value) => + setState(() => privacidadActiva = value), + ), + + const SizedBox(height: 15), + + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.green[50], + borderRadius: BorderRadius.circular(12), + ), + child: const Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.verified_user, color: Colors.green), + SizedBox(width: 10), + Expanded( + child: Text( + 'El sistema solo mostrará información relacionada con los domicilios registrados por el usuario autenticado.', + style: TextStyle(fontSize: 14), + ), + ), + ], + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 25), + + // ── BOTÓN GUARDAR ───────────────────────────────── + SizedBox( + height: 55, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + onPressed: () { + Navigator.pop(context, { + 'inicio' : notificarInicioRuta, + 'aproximacion': notificarCamionCerca, + 'retrasos' : notificarRetrasos, + }); + }, + icon: const Icon(Icons.save), + label: const Text( + 'Guardar Configuración', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), ); } } \ No newline at end of file diff --git a/lib/panel_configuracion_bottom_sheet.dart b/lib/panel_configuracion_bottom_sheet.dart index ac9b352..f9de6e0 100644 --- a/lib/panel_configuracion_bottom_sheet.dart +++ b/lib/panel_configuracion_bottom_sheet.dart @@ -151,7 +151,7 @@ class _PanelConfiguracionBottomSheetState extends State _inicioRuta = value); widget.onAlertasChanged(value, 'inicio'); @@ -160,7 +160,7 @@ class _PanelConfiguracionBottomSheetState extends State _aproximacion = value); widget.onAlertasChanged(value, 'aproximacion'); @@ -169,7 +169,7 @@ class _PanelConfiguracionBottomSheetState extends State _retrasosFallas = value); widget.onAlertasChanged(value, 'retrasos'); diff --git a/lib/tarjetaeta.dart b/lib/tarjetaeta.dart index 6195446..1cc17d6 100644 --- a/lib/tarjetaeta.dart +++ b/lib/tarjetaeta.dart @@ -1,112 +1,194 @@ import 'package:flutter/material.dart'; +import 'camion_estado.dart'; -class TarjetaEtaWidget extends StatelessWidget { - final String horaInicio; - final String horaFin; - final int minutosRestantes; - final String estadoCamion; +class TarjetaEtaWidget extends StatefulWidget { + const TarjetaEtaWidget({super.key}); - const TarjetaEtaWidget({ - super.key, - required this.horaInicio, - required this.horaFin, - required this.minutosRestantes, - required this.estadoCamion, - }); + @override + State createState() => _TarjetaEtaWidgetState(); +} - Color _obtenerColorEstado() { - return estadoCamion == 'retrasado' ? Colors.amber[700]! : Colors.green[700]!; +class _TarjetaEtaWidgetState extends State { + + @override + void initState() { + super.initState(); + // Escucha cambios del estado global + camionEstado.addListener(_actualizar); } - String _generarMensajeAccion() { - if (estadoCamion == 'retrasado') { - return 'El servicio presenta un ligero retraso por tráfico.'; - } - if (minutosRestantes <= 5) { - return '¡Saca tus residuos clasificados ahora mismo!'; - } - return 'Prepara tus bolsas de residuos orgánicos e inorgánicos.'; + void _actualizar() { + if (mounted) setState(() {}); + } + + @override + void dispose() { + camionEstado.removeListener(_actualizar); + super.dispose(); + } + + // Color según etapa + Color _colorEstado() { + final id = camionEstado.positionId; + if (id <= 1) return Colors.grey; + if (id <= 3) return Colors.blue; + if (id <= 5) return Colors.orange; + if (id == 6) return Colors.green; + return Colors.grey; + } + + // Icono según etapa + IconData _iconoEstado() { + final id = camionEstado.positionId; + if (id <= 1) return Icons.schedule; + if (id <= 3) return Icons.local_shipping; + if (id <= 5) return Icons.directions_run; + if (id == 6) return Icons.check_circle; + return Icons.done_all; } @override Widget build(BuildContext context) { - final Color colorTema = _obtenerColorEstado(); + final id = camionEstado.positionId; + final eta = CamionEstado.etaInfo[id]!; + final etapa = CamionEstado.etapas[id]!; + final color = _colorEstado(); + final minutos = eta['minutos'] as int; + final label = eta['label'] as String; return Card( - elevation: 3, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorTema.withOpacity(0.05), - colorTema.withOpacity(0.12), - ], - ), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.local_shipping_rounded, color: colorTema, size: 24), - const SizedBox(width: 8), - Text( - estadoCamion == 'retrasado' ? 'Estado: Demorado' : 'Ruta en Progreso', - style: TextStyle( - color: colorTema, - fontWeight: FontWeight.bold, - fontSize: 13, - ), - ), - const Spacer(), - Container( - width: 8, - height: 8, - decoration: BoxDecoration(color: colorTema, shape: BoxShape.circle), - ), - ], + elevation: 5, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + + // Icono animado según estado + Icon( + _iconoEstado(), + size: 80, + color: color, + ), + + const SizedBox(height: 12), + + const Text( + 'Camión de Recolección', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, ), - const SizedBox(height: 12), + ), + + const SizedBox(height: 8), + + // Etapa actual + Text( + etapa, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: color, + fontWeight: FontWeight.w600, + ), + ), + + const SizedBox(height: 8), + + // Hora estimada + if (id > 1 && id < 7) Text( - 'Llega en aproximadamente $minutosRestantes minutos', - style: Theme.of(context).textTheme.titleMedium?.copyWith( + 'Llegará entre ${eta['horaInicio']} y ${eta['horaFin']}', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 14, color: Colors.grey), + ), + + const SizedBox(height: 16), + + // Barra de progreso + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: LinearProgressIndicator( + value: id / 8, + minHeight: 10, + backgroundColor: Colors.grey[200], + valueColor: AlwaysStoppedAnimation(color), + ), + ), + + const SizedBox(height: 12), + + // Badge de minutos + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(15), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Text( + minutos > 0 + ? 'Llega en aproximadamente $minutos minutos' + : label, + textAlign: TextAlign.center, + style: TextStyle( + color: color, fontWeight: FontWeight.bold, - color: Colors.grey[800], + fontSize: 14, ), ), - const SizedBox(height: 4), - Text( - 'El camión llegará a tu zona entre las $horaInicio y las $horaFin.', - style: TextStyle(fontSize: 14, color: Colors.grey[700]), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 10.0), - child: Divider(color: Colors.black), - ), - Row( - children: [ - Icon(Icons.info_outline_rounded, color: colorTema, size: 18), - const SizedBox(width: 6), - Expanded( - child: Text( - _generarMensajeAccion(), - style: TextStyle( - color: colorTema, - fontWeight: FontWeight.w600, - fontSize: 12, + ), + + const SizedBox(height: 16), + + // Botones iniciar / reiniciar + Row( + children: [ + + Expanded( + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: camionEstado.corriendo + ? Colors.grey + : Colors.green, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), ), + onPressed: camionEstado.corriendo + ? null + : camionEstado.iniciarTimer, + icon: const Icon(Icons.play_arrow), + label: const Text('Iniciar'), ), - ], - ), - ], - ), + ), + + const SizedBox(width: 12), + + Expanded( + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.green, + side: const BorderSide(color: Colors.green), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: camionEstado.reiniciar, + icon: const Icon(Icons.refresh), + label: const Text('Reiniciar'), + ), + ), + ], + ), + ], ), ), );