build + feat: imlementacion de estrucutras de interfaz grafica, construccion del proyecto. Pendiente de arreglar: errores en interfaces

This commit is contained in:
25030248hasel
2026-05-23 02:50:58 -06:00
commit 536f0a1914
147 changed files with 7387 additions and 0 deletions

View File

@@ -0,0 +1,533 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/theme/app_theme.dart';
import '../bloc/auth_bloc.dart';
import '../bloc/auth_event.dart';
import '../bloc/auth_state.dart';
/// Pantalla principal post-login — MVP WasteNotify.
///
/// Cascarón de la pantalla de inicio. En fases futuras contendrá:
/// - ETA de llegada del camión (sin mapa, solo tiempo estimado)
/// - Notificaciones programadas
/// - Historial de recolecciones
/// - Panel de operador (si role == 'operator')
///
/// RESTRICCIÓN DE PRIVACIDAD: Esta pantalla NO mostrará mapas de rutas
/// ni la posición GPS del vehículo. Solo tiempo estimado de llegada.
class HomeScreenPlaceholder extends StatelessWidget {
const HomeScreenPlaceholder({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
final user = state is AuthAuthenticated ? state.user : null;
final isOperator = user?.role == 'operator';
return Scaffold(
backgroundColor: AppTheme.warmWhite,
appBar: AppBar(
title: Row(
children: [
const Icon(Icons.recycling_rounded,
color: AppTheme.leafGreen, size: 22),
const SizedBox(width: 8),
RichText(
text: const TextSpan(
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppTheme.charcoal,
),
children: [
TextSpan(text: 'Waste'),
TextSpan(
text: 'Notify',
style: TextStyle(color: AppTheme.leafGreen),
),
],
),
),
],
),
actions: [
IconButton(
icon: const Icon(Icons.logout_rounded),
tooltip: 'Cerrar sesión',
onPressed: () {
context.read<AuthBloc>().add(const AuthLogoutRequested());
},
),
],
),
body: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.all(20),
sliver: SliverList(
delegate: SliverChildListDelegate([
// --- Bienvenida ---
_WelcomeCard(user: user),
const SizedBox(height: 20),
// --- ETA Principal (cascarón) ---
const _EtaCard(),
const SizedBox(height: 20),
// --- Mensaje preventivo ---
const _PreventiveMessageCard(),
const SizedBox(height: 20),
// --- Próximas funcionalidades ---
_UpcomingFeatures(isOperator: isOperator),
const SizedBox(height: 20),
// --- Info de sesión (debug MVP) ---
if (user != null) _SessionDebugCard(user: user),
]),
),
),
],
),
);
},
);
}
}
// ---------------------------------------------------------------------------
// Sub-widgets de la pantalla home
// ---------------------------------------------------------------------------
class _WelcomeCard extends StatelessWidget {
final dynamic user;
const _WelcomeCard({required this.user});
@override
Widget build(BuildContext context) {
final roleLabel = user?.role == 'operator' ? 'Operador' : 'Ciudadano';
final identifier = user?.email ?? '';
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppTheme.leafGreen, AppTheme.forestGreen],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppTheme.leafGreen.withOpacity(0.3),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: Row(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(13),
),
child: const Icon(
Icons.person_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'¡Bienvenido!',
style: const TextStyle(
color: Colors.white70,
fontSize: 13,
),
),
Text(
identifier,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
roleLabel,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
);
}
}
class _EtaCard extends StatelessWidget {
const _EtaCard();
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.lightMint,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.schedule_rounded,
color: AppTheme.leafGreen,
size: 22,
),
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Tiempo estimado de llegada',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppTheme.charcoal,
),
),
),
],
),
const SizedBox(height: 20),
Center(
child: Column(
children: [
Container(
width: 110,
height: 110,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: AppTheme.lightMint,
width: 6,
),
color: Colors.white,
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.hourglass_empty_rounded,
color: AppTheme.midGray,
size: 28,
),
SizedBox(height: 4),
Text(
'— min',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w800,
color: AppTheme.midGray,
),
),
],
),
),
const SizedBox(height: 14),
Text(
'Notificaciones activas próximamente',
style: TextStyle(
fontSize: 13,
color: AppTheme.midGray,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 5),
decoration: BoxDecoration(
color: AppTheme.lightMint,
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'Fase 2 — En desarrollo',
style: TextStyle(
fontSize: 11,
color: AppTheme.leafGreen,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
),
);
}
}
class _PreventiveMessageCard extends StatelessWidget {
const _PreventiveMessageCard();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.alertAmber.withOpacity(0.08),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: AppTheme.alertAmber.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.campaign_outlined,
color: AppTheme.alertAmber, size: 18),
SizedBox(width: 8),
Text(
'Recuerda siempre',
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 13,
color: AppTheme.earthBrown,
),
),
],
),
const SizedBox(height: 10),
_ReminderItem(
'🚮',
'Saca la basura SOLO cuando recibas la alerta de "próxima llegada".',
),
_ReminderItem(
'🚫',
'Nunca persigas ni te acerques al camión. El sistema te avisará a tiempo.',
),
_ReminderItem(
'🌱',
'Separar tus residuos hace más eficiente la recolección. ¡Gracias!',
),
],
),
);
}
}
class _ReminderItem extends StatelessWidget {
final String emoji;
final String text;
const _ReminderItem(this.emoji, this.text);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(emoji, style: const TextStyle(fontSize: 14)),
const SizedBox(width: 8),
Expanded(
child: Text(
text,
style: const TextStyle(
fontSize: 12.5,
color: AppTheme.earthBrown,
height: 1.4,
),
),
),
],
),
);
}
}
class _UpcomingFeatures extends StatelessWidget {
final bool isOperator;
const _UpcomingFeatures({required this.isOperator});
@override
Widget build(BuildContext context) {
final features = [
(Icons.notifications_active_outlined, 'Alertas push de recolección',
'Fase 2'),
(Icons.history_rounded, 'Historial de notificaciones', 'Fase 2'),
(Icons.settings_outlined, 'Configurar zona y horario', 'Fase 3'),
if (isOperator) ...[
(Icons.bar_chart_rounded, 'Panel de rutas completadas', 'Fase 3'),
(Icons.group_outlined, 'Gestión de sectores', 'Fase 4'),
],
];
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Próximamente en WasteNotify',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppTheme.charcoal,
),
),
const SizedBox(height: 12),
...features.map((f) => _FeatureRow(
icon: f.$1,
label: f.$2,
phase: f.$3,
)),
],
),
),
);
}
}
class _FeatureRow extends StatelessWidget {
final IconData icon;
final String label;
final String phase;
const _FeatureRow({
required this.icon,
required this.label,
required this.phase,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
Icon(icon, size: 18, color: AppTheme.mintGreen),
const SizedBox(width: 12),
Expanded(
child: Text(
label,
style: const TextStyle(fontSize: 13, color: AppTheme.charcoal),
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppTheme.lightGray,
borderRadius: BorderRadius.circular(20),
),
child: Text(
phase,
style: const TextStyle(
fontSize: 10,
color: AppTheme.midGray,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
}
class _SessionDebugCard extends StatelessWidget {
final dynamic user;
const _SessionDebugCard({required this.user});
@override
Widget build(BuildContext context) {
final token = user?.token ?? '';
final tokenPreview =
token.length > 40 ? '${token.substring(0, 40)}' : token;
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFF3E5F5),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFCE93D8), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.developer_mode_rounded,
size: 14, color: Color(0xFF7B1FA2)),
SizedBox(width: 6),
Text(
'Debug — Sesión JWT (solo MVP)',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Color(0xFF7B1FA2),
),
),
],
),
const SizedBox(height: 6),
Text(
'Token: $tokenPreview',
style: const TextStyle(
fontSize: 10.5,
fontFamily: 'monospace',
color: Color(0xFF4A148C),
),
),
Text(
'Role: ${user?.role} | Expira: ${user?.expiresAt?.toLocal().toString().substring(0, 16)}',
style: const TextStyle(
fontSize: 10.5,
fontFamily: 'monospace',
color: Color(0xFF4A148C),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/theme/app_theme.dart';
import '../bloc/auth_bloc.dart';
import '../bloc/auth_event.dart';
import '../bloc/auth_state.dart';
import '../widgets/privacy_notice_card.dart';
/// Pantalla de inicio de sesión — MVP WasteNotify.
///
/// Diseño: Limpio, institucional, con paleta verde-tierra.
/// Incluye mensajería preventiva integrada de forma no intrusiva.
///
/// Credenciales de demo:
/// Ciudadano: ciudadano@ejemplo.com / password123
/// Operador: operador@ejemplo.com / operador456
/// Teléfono: 5551234567 / pass1234
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _identifierController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
late final AnimationController _animController;
late final Animation<double> _fadeAnim;
late final Animation<Offset> _slideAnim;
@override
void initState() {
super.initState();
_animController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 700),
);
_fadeAnim = CurvedAnimation(
parent: _animController,
curve: Curves.easeOut,
);
_slideAnim = Tween<Offset>(
begin: const Offset(0, 0.06),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animController,
curve: Curves.easeOutCubic,
));
_animController.forward();
}
@override
void dispose() {
_animController.dispose();
_identifierController.dispose();
_passwordController.dispose();
super.dispose();
}
void _submit(BuildContext context) {
if (_formKey.currentState?.validate() ?? false) {
context.read<AuthBloc>().add(
AuthLoginRequested(
identifier: _identifierController.text,
password: _passwordController.text,
),
);
}
}
@override
Widget build(BuildContext context) {
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error_outline, color: Colors.white, size: 18),
const SizedBox(width: 10),
Expanded(child: Text(state.message)),
],
),
backgroundColor: AppTheme.errorRed,
),
);
}
},
child: Scaffold(
backgroundColor: AppTheme.warmWhite,
body: SafeArea(
child: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: FadeTransition(
opacity: _fadeAnim,
child: SlideTransition(
position: _slideAnim,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 48),
_buildHeader(),
const SizedBox(height: 32),
const ScheduleWarningBanner(),
const SizedBox(height: 32),
_buildForm(context),
const SizedBox(height: 24),
const PrivacyNoticeCard(),
const SizedBox(height: 32),
_buildDemoHint(),
const SizedBox(height: 24),
],
),
),
), const SizedBox(height: 8),
Text(
'Sistema Municipal de Recolección Residencial\nCelaya, Guanajuato',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12, color: Colors.grey.shade600, height: 1.3),
),
const SizedBox(height: 48),
// --- CAMPO: EMAIL / IDENTIFICADOR ---
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Correo Electrónico o Teléfono',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12))),
),
validator: (value) =>
(value == null || value.trim().isEmpty)
? 'Por favor, ingresa tus datos de acceso'
: null,
),
const SizedBox(height: 20),
// --- CAMPO: CONTRASENA ---
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Contraseña de Acceso',
prefixIcon: Icon(Icons.lock_outline_rounded),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12))),
),
validator: (value) => (value == null || value.isEmpty)
? 'Por favor, introduce tu contraseña'
: null,
),
const SizedBox(height: 32),
// --- CONTENEDOR REACTIVO DE BOTONES (LOGIN & REGISTER) ---
// --- CONTENEDOR REACTIVO DE BOTONES (LOGIN & REGISTER) ---
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
final isLoading = state is AuthLoading;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: isLoading
? []
: [
BoxShadow(
color: AppTheme.leafGreen
.withOpacity(0.3),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: ElevatedButton(
onPressed:
isLoading ? null : () => _submit(context),
style: ElevatedButton.styleFrom(
backgroundColor: isLoading
? AppTheme.mintGreen.withOpacity(0.7)
: AppTheme.leafGreen,
disabledBackgroundColor:
AppTheme.mintGreen.withOpacity(0.6),
padding:
const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation(
Colors.white),
),
)
: const Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(Icons.login_rounded,
size: 20, color: Colors.white),
SizedBox(width: 10),
Text(
'Ingresar de forma segura',
style: TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.bold),
),
],
),
),
),
// 📍 ENLACE DE REDIRECCIÓN AL REGISTRO CIUDADANO
const SizedBox(height: 20),
TextButton(
onPressed: () {
// 🚀 Forzamos la navegación limpia usando la ruta de texto directo
context.push('/register');
},
child: const Text(
'¿No tienes una cuenta ciudadana? Regístrate aquí',
style: TextStyle(
color: AppTheme.leafGreen,
fontWeight: FontWeight.w700,
fontSize: 13,
),
),
)
]);
},
),
], // Fin children de la Column
), // Fin Column
), // Fin Form
), // Fin SingleChildScrollView
), // Fin SafeArea
), // Fin BlocListener
); // Fin Scaffold (Asegúrate de que tenga el punto y coma aquí)

View File

@@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_application_1/features/auth/data/models/mock_waste_data.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
String _selectedColonia = 'Centro';
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Crear Cuenta Ciudadana')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(Icons.assignment_ind_outlined, size: 80, color: Colors.green),
const SizedBox(height: 16),
Text(
'Regístrate para recibir avisos de recolección en tu zona',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 24),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Nombre Completo', border: OutlineInputBorder()),
validator: (v) => v!.isEmpty ? 'Ingresa tu nombre' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Correo o Teléfono', border: OutlineInputBorder()),
validator: (v) => v!.isEmpty ? 'Ingresa tus datos' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(labelText: 'Contraseña', border: OutlineInputBorder()),
validator: (v) => v!.length < 6 ? 'Mínimo 6 caracteres' : null,
),
const SizedBox(height: 16),
// Dropdown para seleccionar la Colonia (Requisito MVP)
DropdownButtonFormField<String>(
value: _selectedColonia,
decoration: const InputDecoration(labelText: 'Selecciona tu Colonia / Domicilio', border: OutlineInputBorder()),
items: MockWasteData.schedules.map((zone) {
return DropdownMenuItem(value: zone.colonia, child: Text(zone.colonia));
}).toList(),
onChanged: (val) => setState(() => _selectedColonia = val!),
),
const SizedBox(height: 24),
// Mensajería Preventiva Legal
Card(
color: Colors.amber.shade50,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
'🔒 Privacidad Garantizada:\nAl registrar tu domicilio, tu cuenta operará bajo el principio de "Visión de Túnel". Solo verás alertas de tu sector. Está prohibido el rastreo de flotillas.',
style: TextStyle(color: Colors.amber.shade900, fontSize: 13, fontWeight: FontWeight.bold),
),
),
),
const SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: Colors.green),
onPressed: () {
if (_formKey.currentState!.validate()) {
// Simulación de Registro Exitoso: Navega al Home enviando la colonia elegida
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Cuenta creada con éxito (Modo Simulación)')),
);
context.go('/home?colonia=$_selectedColonia');
}
},
child: const Text('Registrarse', style: TextStyle(color: Colors.white, fontSize: 16)),
),
],
),
),
),
);
}
}