vistas de mockop
This commit is contained in:
2443
views_v1/admin_screen.dart
Normal file
2443
views_v1/admin_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
388
views_v1/alerts_screen.dart
Normal file
388
views_v1/alerts_screen.dart
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
import '../models/models.dart';
|
||||||
|
import '../widgets/widgets.dart' as w;
|
||||||
|
|
||||||
|
class AlertsScreen extends StatefulWidget {
|
||||||
|
const AlertsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AlertsScreen> createState() => _AlertsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlertsScreenState extends State<AlertsScreen> {
|
||||||
|
// Alerta activa de ejemplo
|
||||||
|
final AlertaModel _alertaActiva = AlertaModel(
|
||||||
|
id: 'alerta-001',
|
||||||
|
tipo: TipoAlerta.cercana,
|
||||||
|
distanciaMetros: 180,
|
||||||
|
fecha: DateTime.now(),
|
||||||
|
direccionCasa: 'Av. Insurgentes 245',
|
||||||
|
leida: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Historial de ejemplo
|
||||||
|
final List<AlertaModel> _historial = [
|
||||||
|
AlertaModel(
|
||||||
|
id: 'h-001',
|
||||||
|
tipo: TipoAlerta.cercana,
|
||||||
|
distanciaMetros: 200,
|
||||||
|
fecha: DateTime.now().subtract(const Duration(hours: 1)),
|
||||||
|
direccionCasa: 'Av. Insurgentes 245',
|
||||||
|
leida: true,
|
||||||
|
),
|
||||||
|
AlertaModel(
|
||||||
|
id: 'h-002',
|
||||||
|
tipo: TipoAlerta.cercana,
|
||||||
|
distanciaMetros: 200,
|
||||||
|
fecha: DateTime.now().subtract(const Duration(days: 2, hours: 2)),
|
||||||
|
direccionCasa: 'Av. Insurgentes 245',
|
||||||
|
leida: true,
|
||||||
|
),
|
||||||
|
AlertaModel(
|
||||||
|
id: 'h-003',
|
||||||
|
tipo: TipoAlerta.cercana,
|
||||||
|
distanciaMetros: 200,
|
||||||
|
fecha: DateTime.now().subtract(const Duration(days: 4, hours: 1, minutes: 30)),
|
||||||
|
direccionCasa: 'Av. Insurgentes 245',
|
||||||
|
leida: true,
|
||||||
|
),
|
||||||
|
AlertaModel(
|
||||||
|
id: 'h-004',
|
||||||
|
tipo: TipoAlerta.cercana,
|
||||||
|
distanciaMetros: 200,
|
||||||
|
fecha: DateTime.now().subtract(const Duration(days: 7, hours: 3)),
|
||||||
|
direccionCasa: 'Av. Insurgentes 245',
|
||||||
|
leida: true,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppTheme.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Alertas'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Text('Limpiar',
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 13)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: RefreshIndicator(
|
||||||
|
color: AppTheme.primary,
|
||||||
|
onRefresh: () async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 800));
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
// ── Alerta activa ───────────────────────────────────────────
|
||||||
|
if (_alertaActiva != null) ...[
|
||||||
|
_AlertaActivaCard(alerta: _alertaActiva!),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── Historial ────────────────────────────────────────────────
|
||||||
|
if (_historial.isEmpty)
|
||||||
|
_EmptyState()
|
||||||
|
else ...[
|
||||||
|
w.SectionTitle(title: 'Historial de alertas'),
|
||||||
|
..._historial.map((a) => _AlertaHistorialItem(alerta: a)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tarjeta de alerta activa ──────────────────────────────────────────────────
|
||||||
|
class _AlertaActivaCard extends StatelessWidget {
|
||||||
|
final AlertaModel alerta;
|
||||||
|
const _AlertaActivaCard({required this.alerta});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final progreso = (1 - (alerta.distanciaMetros / 400)).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryLight,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
border: Border.all(color: AppTheme.primaryMid),
|
||||||
|
boxShadow: AppTheme.softShadow,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primary,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.notifications_active,
|
||||||
|
color: Colors.white, size: 22),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('¡El camión está cerca!',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppTheme.primaryDark)),
|
||||||
|
Text(alerta.fechaFormateada,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12, color: AppTheme.primary)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primary,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: const Text('Ahora',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Distancia
|
||||||
|
Text(
|
||||||
|
'El camión se encuentra a',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13, color: AppTheme.primaryDark),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
alerta.distanciaTexto,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppTheme.primary,
|
||||||
|
height: 1.1),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 6),
|
||||||
|
child: Text(
|
||||||
|
'de tu casa en ${alerta.direccionCasa}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13, color: AppTheme.primaryDark),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
// Tiempo estimado
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text('Llegada estimada:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: AppTheme.primaryDark)),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
alerta.tiempoEstimadoTexto,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppTheme.primary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Barra de progreso
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progreso,
|
||||||
|
backgroundColor: AppTheme.primaryMid.withValues(alpha: 0.4),
|
||||||
|
valueColor: const AlwaysStoppedAnimation<Color>(AppTheme.primary),
|
||||||
|
minHeight: 7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
children: const [
|
||||||
|
Text('Lejos',
|
||||||
|
style: TextStyle(fontSize: 10, color: AppTheme.primary)),
|
||||||
|
Spacer(),
|
||||||
|
Text('Tu casa',
|
||||||
|
style: TextStyle(fontSize: 10, color: AppTheme.primary)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
// Botón ver en mapa
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {},
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primary,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.map_outlined, color: Colors.white, size: 16),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text('Ver en el mapa',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ítem de historial ─────────────────────────────────────────────────────────
|
||||||
|
class _AlertaHistorialItem extends StatelessWidget {
|
||||||
|
final AlertaModel alerta;
|
||||||
|
const _AlertaHistorialItem({required this.alerta});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
border: Border.all(color: AppTheme.border, width: 0.5),
|
||||||
|
boxShadow: AppTheme.softShadow,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.background,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.notifications_outlined,
|
||||||
|
color: AppTheme.textSecondary, size: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Camión a ${alerta.distanciaTexto}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppTheme.textPrimary),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(alerta.fechaFormateada,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12, color: AppTheme.textSecondary)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_EtiquetaDia(texto: alerta.etiquetaFecha),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Etiqueta de día ───────────────────────────────────────────────────────────
|
||||||
|
class _EtiquetaDia extends StatelessWidget {
|
||||||
|
final String texto;
|
||||||
|
const _EtiquetaDia({required this.texto});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final esHoy = texto == 'Hoy';
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: esHoy ? AppTheme.primaryLight : AppTheme.background,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
texto,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: esHoy ? AppTheme.primaryDark : AppTheme.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Estado vacío ──────────────────────────────────────────────────────────────
|
||||||
|
class _EmptyState extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 60),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryLight,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.notifications_outlined,
|
||||||
|
color: AppTheme.primary, size: 34),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text('Sin alertas por ahora',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppTheme.textPrimary)),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
const Text(
|
||||||
|
'Te notificaremos cuando el camión\nesté cerca de tu casa.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: AppTheme.textSecondary, height: 1.5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1631
views_v1/driver_screen.dart
Normal file
1631
views_v1/driver_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
495
views_v1/house_screen.dart
Normal file
495
views_v1/house_screen.dart
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
import '../models/models.dart';
|
||||||
|
import '../widgets/widgets.dart' as w;
|
||||||
|
|
||||||
|
class MyHouseScreen extends StatefulWidget {
|
||||||
|
const MyHouseScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyHouseScreen> createState() => _MyHouseScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyHouseScreenState extends State<MyHouseScreen> {
|
||||||
|
HouseModel _casa = const HouseModel(
|
||||||
|
id: 'casa-01',
|
||||||
|
calle: 'Av. Insurgentes 245',
|
||||||
|
colonia: 'Centro',
|
||||||
|
codigoPostal: '38000',
|
||||||
|
latitud: 20.5226,
|
||||||
|
longitud: -100.8191,
|
||||||
|
radioAlertaMetros: 200,
|
||||||
|
alertaCercana: true,
|
||||||
|
alertaMedia: false,
|
||||||
|
recordatorioDiario: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppTheme.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Mi casa'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
onPressed: () => _mostrarEditarDireccion(context),
|
||||||
|
tooltip: 'Editar dirección',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
// ── Tarjeta de la casa ──────────────────────────────────────
|
||||||
|
_CasaCard(casa: _casa),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── Configuración de radio ──────────────────────────────────
|
||||||
|
w.SectionTitle(title: 'Radio de alerta'),
|
||||||
|
_RadioAlertaCard(
|
||||||
|
radioActual: _casa.radioAlertaMetros,
|
||||||
|
onChanged: (v) => setState(() {
|
||||||
|
_casa = _casa.copyWith(radioAlertaMetros: v);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── Notificaciones ──────────────────────────────────────────
|
||||||
|
w.SectionTitle(title: 'Notificaciones'),
|
||||||
|
_NotificacionesCard(
|
||||||
|
casa: _casa,
|
||||||
|
onAlertaCercanaChanged: (v) =>
|
||||||
|
setState(() => _casa = _casa.copyWith(alertaCercana: v)),
|
||||||
|
onAlertaMediaChanged: (v) =>
|
||||||
|
setState(() => _casa = _casa.copyWith(alertaMedia: v)),
|
||||||
|
onRecordatorioChanged: (v) =>
|
||||||
|
setState(() => _casa = _casa.copyWith(recordatorioDiario: v)),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── Horario estimado ────────────────────────────────────────
|
||||||
|
w.SectionTitle(title: 'Horario del camión'),
|
||||||
|
_HorarioCard(),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── Agregar otra casa ───────────────────────────────────────
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _mostrarAgregarCasa(context),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppTheme.primaryMid,
|
||||||
|
width: 1,
|
||||||
|
style: BorderStyle.solid),
|
||||||
|
boxShadow: AppTheme.softShadow,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Icon(Icons.add_home_outlined,
|
||||||
|
color: AppTheme.primary, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Agregar otra dirección',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppTheme.primary)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _mostrarEditarDireccion(BuildContext context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: AppTheme.surface,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: Radius.circular(AppTheme.radiusXl)),
|
||||||
|
),
|
||||||
|
builder: (_) => _EditarDireccionSheet(casa: _casa),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _mostrarAgregarCasa(BuildContext context) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Funcionalidad próximamente disponible'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: AppTheme.primary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tarjeta principal de la casa ──────────────────────────────────────────────
|
||||||
|
class _CasaCard extends StatelessWidget {
|
||||||
|
final HouseModel casa;
|
||||||
|
const _CasaCard({required this.casa});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
border: Border.all(color: AppTheme.primaryMid, width: 0.8),
|
||||||
|
boxShadow: AppTheme.softShadow,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryLight,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.home_outlined,
|
||||||
|
color: AppTheme.primary, size: 24),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(casa.alias,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppTheme.textPrimary)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
w.StatusBadge.green(
|
||||||
|
casa.activa ? 'Activa' : 'Inactiva'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.more_vert,
|
||||||
|
color: AppTheme.textSecondary, size: 20),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
const Divider(color: AppTheme.borderLight),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
// Detalles
|
||||||
|
_DetailRow(
|
||||||
|
icon: Icons.location_on_outlined,
|
||||||
|
text: casa.direccionCompleta,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_DetailRow(
|
||||||
|
icon: Icons.my_location_outlined,
|
||||||
|
text:
|
||||||
|
'${casa.latitud.toStringAsFixed(4)}, ${casa.longitud.toStringAsFixed(4)}',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_DetailRow(
|
||||||
|
icon: Icons.radar_outlined,
|
||||||
|
text: 'Alerta a ${casa.radioAlertaMetros} m de distancia',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DetailRow extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String text;
|
||||||
|
const _DetailRow({required this.icon, required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 15, color: AppTheme.textSecondary),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(text,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13, color: AppTheme.textSecondary, height: 1.4)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Radio de alerta ───────────────────────────────────────────────────────────
|
||||||
|
class _RadioAlertaCard extends StatelessWidget {
|
||||||
|
final int radioActual;
|
||||||
|
final ValueChanged<int> onChanged;
|
||||||
|
const _RadioAlertaCard({required this.radioActual, required this.onChanged});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
border: Border.all(color: AppTheme.border, width: 0.5),
|
||||||
|
boxShadow: AppTheme.softShadow,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [200, 400, 600].map((dist) {
|
||||||
|
final selected = dist == radioActual;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onChanged(dist),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected ? AppTheme.primaryLight : AppTheme.background,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
border: Border.all(
|
||||||
|
color: selected ? AppTheme.primary : AppTheme.border,
|
||||||
|
width: selected ? 1.5 : 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
selected
|
||||||
|
? Icons.radio_button_checked
|
||||||
|
: Icons.radio_button_unchecked,
|
||||||
|
color: selected ? AppTheme.primary : AppTheme.border,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'$dist metros',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: selected
|
||||||
|
? AppTheme.primaryDark
|
||||||
|
: AppTheme.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (selected)
|
||||||
|
Text(
|
||||||
|
dist == 200
|
||||||
|
? '~2-3 min'
|
||||||
|
: dist == 400
|
||||||
|
? '~4-5 min'
|
||||||
|
: '~6-8 min',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: AppTheme.primary,
|
||||||
|
fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Notificaciones ────────────────────────────────────────────────────────────
|
||||||
|
class _NotificacionesCard extends StatelessWidget {
|
||||||
|
final HouseModel casa;
|
||||||
|
final ValueChanged<bool> onAlertaCercanaChanged;
|
||||||
|
final ValueChanged<bool> onAlertaMediaChanged;
|
||||||
|
final ValueChanged<bool> onRecordatorioChanged;
|
||||||
|
|
||||||
|
const _NotificacionesCard({
|
||||||
|
required this.casa,
|
||||||
|
required this.onAlertaCercanaChanged,
|
||||||
|
required this.onAlertaMediaChanged,
|
||||||
|
required this.onRecordatorioChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
border: Border.all(color: AppTheme.border, width: 0.5),
|
||||||
|
boxShadow: AppTheme.softShadow,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
w.LabeledSwitch(
|
||||||
|
label: 'Alerta cuando el camión esté cerca',
|
||||||
|
value: casa.alertaCercana,
|
||||||
|
onChanged: onAlertaCercanaChanged,
|
||||||
|
),
|
||||||
|
const Divider(height: 1, color: AppTheme.borderLight),
|
||||||
|
w.LabeledSwitch(
|
||||||
|
label: 'Alerta a distancia media',
|
||||||
|
value: casa.alertaMedia,
|
||||||
|
onChanged: onAlertaMediaChanged,
|
||||||
|
),
|
||||||
|
const Divider(height: 1, color: AppTheme.borderLight),
|
||||||
|
w.LabeledSwitch(
|
||||||
|
label: 'Recordatorio diario del horario',
|
||||||
|
value: casa.recordatorioDiario,
|
||||||
|
onChanged: onRecordatorioChanged,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Horario del camión ────────────────────────────────────────────────────────
|
||||||
|
class _HorarioCard extends StatelessWidget {
|
||||||
|
final List<_HorarioDia> _dias = const [
|
||||||
|
_HorarioDia(dia: 'Lunes', hora: '8:00 – 10:00 a.m.', activo: true),
|
||||||
|
_HorarioDia(dia: 'Martes', hora: '8:00 – 10:00 a.m.', activo: true),
|
||||||
|
_HorarioDia(dia: 'Miércoles', hora: 'Sin servicio', activo: false),
|
||||||
|
_HorarioDia(dia: 'Jueves', hora: '8:00 – 10:00 a.m.', activo: true),
|
||||||
|
_HorarioDia(dia: 'Viernes', hora: '8:00 – 10:00 a.m.', activo: true),
|
||||||
|
_HorarioDia(dia: 'Sábado', hora: '9:00 – 11:00 a.m.', activo: true),
|
||||||
|
_HorarioDia(dia: 'Domingo', hora: 'Sin servicio', activo: false),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
border: Border.all(color: AppTheme.border, width: 0.5),
|
||||||
|
boxShadow: AppTheme.softShadow,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: _dias.map((d) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(d.dia,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: d.activo
|
||||||
|
? AppTheme.textPrimary
|
||||||
|
: AppTheme.textSecondary)),
|
||||||
|
const Spacer(),
|
||||||
|
Text(d.hora,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: d.activo
|
||||||
|
? AppTheme.primary
|
||||||
|
: AppTheme.textSecondary)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HorarioDia {
|
||||||
|
final String dia;
|
||||||
|
final String hora;
|
||||||
|
final bool activo;
|
||||||
|
const _HorarioDia(
|
||||||
|
{required this.dia, required this.hora, required this.activo});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sheet de editar dirección ─────────────────────────────────────────────────
|
||||||
|
class _EditarDireccionSheet extends StatelessWidget {
|
||||||
|
final HouseModel casa;
|
||||||
|
const _EditarDireccionSheet({required this.casa});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 24, right: 24, top: 24,
|
||||||
|
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Handle
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 36, height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.border,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
const Text('Editar dirección',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppTheme.textPrimary)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
w.FormField(
|
||||||
|
label: 'Calle y número', initialValue: casa.calle),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: w.FormField(
|
||||||
|
label: 'Colonia', initialValue: casa.colonia),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: w.FormField(
|
||||||
|
label: 'C.P.', initialValue: casa.codigoPostal),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Guardar cambios'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
290
views_v1/login_screen.dart
Normal file
290
views_v1/login_screen.dart
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
import '../widgets/widgets.dart' as w;
|
||||||
|
import 'admin_screen.dart';
|
||||||
|
import 'driver_screen.dart';
|
||||||
|
import 'main_shell.dart';
|
||||||
|
|
||||||
|
enum UserRole { usuario, conductor, administrador }
|
||||||
|
|
||||||
|
class LoginScreen extends StatefulWidget {
|
||||||
|
const LoginScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LoginScreen> createState() => _LoginScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginScreenState extends State<LoginScreen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _emailCtrl = TextEditingController();
|
||||||
|
final _passCtrl = TextEditingController();
|
||||||
|
UserRole _selectedRole = UserRole.usuario;
|
||||||
|
bool _obscurePass = true;
|
||||||
|
bool _loading = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_emailCtrl.dispose();
|
||||||
|
_passCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _login() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
setState(() => _loading = true);
|
||||||
|
await Future.delayed(const Duration(seconds: 1)); // Simular petición
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _loading = false);
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => _homeForRole()),
|
||||||
|
(_) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _homeForRole() {
|
||||||
|
switch (_selectedRole) {
|
||||||
|
case UserRole.conductor:
|
||||||
|
return const DriverShell();
|
||||||
|
case UserRole.administrador:
|
||||||
|
return const AdminShell();
|
||||||
|
case UserRole.usuario:
|
||||||
|
return const MainShell();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppTheme.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
iconTheme: const IconThemeData(color: AppTheme.textPrimary),
|
||||||
|
title: const Text(
|
||||||
|
'Iniciar sesión',
|
||||||
|
style: TextStyle(color: AppTheme.textPrimary, fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// ── Encabezado ─────────────────────────────────────────
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryLight,
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.delete_outline_rounded,
|
||||||
|
color: AppTheme.primary, size: 26),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
const Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('RutaVerde',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppTheme.textPrimary)),
|
||||||
|
Text('Bienvenido de nuevo',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: AppTheme.textSecondary)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// ── Formulario ─────────────────────────────────────────
|
||||||
|
w.FormField(
|
||||||
|
label: 'Correo electrónico',
|
||||||
|
hint: 'tu@correo.com',
|
||||||
|
controller: _emailCtrl,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
w.FormField(
|
||||||
|
label: 'Contraseña',
|
||||||
|
hint: '••••••••',
|
||||||
|
controller: _passCtrl,
|
||||||
|
obscureText: _obscurePass,
|
||||||
|
suffix: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscurePass
|
||||||
|
? Icons.visibility_outlined
|
||||||
|
: Icons.visibility_off_outlined,
|
||||||
|
size: 18,
|
||||||
|
color: AppTheme.textSecondary,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
setState(() => _obscurePass = !_obscurePass),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<UserRole>(
|
||||||
|
initialValue: _selectedRole,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Tipo de usuario',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 14, vertical: 16),
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: UserRole.usuario,
|
||||||
|
child: Text('Usuario'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: UserRole.conductor,
|
||||||
|
child: Text('Conductor'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: UserRole.administrador,
|
||||||
|
child: Text('Administrador'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
setState(() => _selectedRole = value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {},
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: AppTheme.primary),
|
||||||
|
child: const Text('¿Olvidaste tu contraseña?',
|
||||||
|
style: TextStyle(fontSize: 13)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// ── Botón ingresar ──────────────────────────────────────
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 52,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _loading ? null : _login,
|
||||||
|
child: _loading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2, color: Colors.white),
|
||||||
|
)
|
||||||
|
: const Text('Ingresar'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
|
// ── Divisor ─────────────────────────────────────────────
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(child: Divider(color: AppTheme.border)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: Text('o',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: AppTheme.textSecondary)),
|
||||||
|
),
|
||||||
|
const Expanded(child: Divider(color: AppTheme.border)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// ── Continuar con Google ────────────────────────────────
|
||||||
|
_SocialButton(
|
||||||
|
icon: Icons.g_mobiledata_rounded,
|
||||||
|
label: 'Continuar con Google',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 36),
|
||||||
|
|
||||||
|
// ── Crear cuenta ────────────────────────────────────────
|
||||||
|
Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('¿No tienes cuenta? ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: AppTheme.textSecondary)),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => Navigator.pop(context),
|
||||||
|
child: const Text('Regístrate',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppTheme.primary)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SocialButton extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _SocialButton(
|
||||||
|
{required this.icon, required this.label, required this.onTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 13),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
border: Border.all(color: AppTheme.border),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 22, color: AppTheme.textPrimary),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppTheme.textPrimary)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
views_v1/main_shell.dart
Normal file
38
views_v1/main_shell.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../widgets/widgets.dart' as w;
|
||||||
|
import 'map_screen.dart';
|
||||||
|
import 'alerts_screen.dart';
|
||||||
|
import 'house_screen.dart';
|
||||||
|
import 'profile_screen.dart';
|
||||||
|
|
||||||
|
class MainShell extends StatefulWidget {
|
||||||
|
const MainShell({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MainShell> createState() => _MainShellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MainShellState extends State<MainShell> {
|
||||||
|
int _currentIndex = 0;
|
||||||
|
|
||||||
|
final List<Widget> _screens = const [
|
||||||
|
MapScreen(),
|
||||||
|
AlertsScreen(),
|
||||||
|
MyHouseScreen(),
|
||||||
|
ProfileScreen(),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: IndexedStack(
|
||||||
|
index: _currentIndex,
|
||||||
|
children: _screens,
|
||||||
|
),
|
||||||
|
bottomNavigationBar: w.AppBottomNav(
|
||||||
|
currentIndex: _currentIndex,
|
||||||
|
onTap: (i) => setState(() => _currentIndex = i),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
381
views_v1/map_screen.dart
Normal file
381
views_v1/map_screen.dart
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
import '../models/models.dart';
|
||||||
|
import '../widgets/widgets.dart' as w;
|
||||||
|
|
||||||
|
class MapScreen extends StatefulWidget {
|
||||||
|
const MapScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MapScreen> createState() => _MapScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapScreenState extends State<MapScreen> {
|
||||||
|
final Completer<GoogleMapController> _mapController = Completer();
|
||||||
|
|
||||||
|
// Coordenadas de ejemplo — Celaya, Gto.
|
||||||
|
static const LatLng _casaPos = LatLng(20.5226, -100.8191);
|
||||||
|
static const LatLng _camionPos = LatLng(20.5255, -100.8220);
|
||||||
|
static const CameraPosition _camaraInicial = CameraPosition(
|
||||||
|
target: LatLng(20.5240, -100.8205),
|
||||||
|
zoom: 15.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Datos de ejemplo del camión
|
||||||
|
final TruckLocation _camion = TruckLocation(
|
||||||
|
id: 'truck-01',
|
||||||
|
ruta: 'Ruta Norte',
|
||||||
|
latitud: _camionPos.latitude,
|
||||||
|
longitud: _camionPos.longitude,
|
||||||
|
ultimaActualizacion: DateTime.now().subtract(const Duration(seconds: 28)),
|
||||||
|
enServicio: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final HouseModel _casa = HouseModel(
|
||||||
|
id: 'casa-01',
|
||||||
|
calle: 'Av. Insurgentes 245',
|
||||||
|
colonia: 'Centro',
|
||||||
|
codigoPostal: '38000',
|
||||||
|
latitud: _casaPos.latitude,
|
||||||
|
longitud: _casaPos.longitude,
|
||||||
|
radioAlertaMetros: 200,
|
||||||
|
);
|
||||||
|
|
||||||
|
Set<Marker> _markers = {};
|
||||||
|
Set<Circle> _circles = {};
|
||||||
|
Timer? _refreshTimer;
|
||||||
|
|
||||||
|
// Distancia simulada (metros)
|
||||||
|
double get _distanciaMetros => 380;
|
||||||
|
int get _minutosEstimados => 8;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_buildMapElements();
|
||||||
|
// Simular actualización de posición cada 30s
|
||||||
|
_refreshTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_refreshTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _buildMapElements() {
|
||||||
|
_markers = {
|
||||||
|
Marker(
|
||||||
|
markerId: const MarkerId('camion'),
|
||||||
|
position: LatLng(_camion.latitud, _camion.longitud),
|
||||||
|
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
|
||||||
|
infoWindow: InfoWindow(
|
||||||
|
title: 'Camión · ${_camion.ruta}',
|
||||||
|
snippet: _camion.tiempoActualizacion,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Marker(
|
||||||
|
markerId: const MarkerId('casa'),
|
||||||
|
position: LatLng(_casa.latitud, _casa.longitud),
|
||||||
|
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),
|
||||||
|
infoWindow: InfoWindow(title: _casa.alias, snippet: _casa.calle),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
_circles = {
|
||||||
|
Circle(
|
||||||
|
circleId: const CircleId('radio-alerta'),
|
||||||
|
center: LatLng(_casa.latitud, _casa.longitud),
|
||||||
|
radius: _casa.radioAlertaMetros.toDouble(),
|
||||||
|
fillColor: AppTheme.blue.withValues(alpha: 0.08),
|
||||||
|
strokeColor: AppTheme.blue.withValues(alpha: 0.4),
|
||||||
|
strokeWidth: 1,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _centrarMapa() async {
|
||||||
|
final controller = await _mapController.future;
|
||||||
|
await controller.animateCamera(
|
||||||
|
CameraUpdate.newCameraPosition(_camaraInicial),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppTheme.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Rastreo en vivo'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.my_location),
|
||||||
|
onPressed: _centrarMapa,
|
||||||
|
tooltip: 'Centrar mapa',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// ── Mapa ─────────────────────────────────────────────────────
|
||||||
|
Expanded(
|
||||||
|
flex: 5,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
GoogleMap(
|
||||||
|
initialCameraPosition: _camaraInicial,
|
||||||
|
markers: _markers,
|
||||||
|
circles: _circles,
|
||||||
|
myLocationButtonEnabled: false,
|
||||||
|
zoomControlsEnabled: false,
|
||||||
|
mapType: MapType.normal,
|
||||||
|
onMapCreated: (c) {
|
||||||
|
_mapController.complete(c);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// Indicador "En vivo"
|
||||||
|
Positioned(
|
||||||
|
top: 14,
|
||||||
|
right: 14,
|
||||||
|
child: _LiveBadge(activo: _camion.enServicio),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Actualización
|
||||||
|
Positioned(
|
||||||
|
top: 14,
|
||||||
|
left: 14,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: AppTheme.softShadow,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.refresh,
|
||||||
|
size: 14, color: AppTheme.textSecondary),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_camion.tiempoActualizacion,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: AppTheme.textSecondary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Panel inferior ────────────────────────────────────────────
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.background,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(AppTheme.radiusXl)),
|
||||||
|
boxShadow: AppTheme.cardShadow,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Handle
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
width: 36,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.border,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Camión
|
||||||
|
w.InfoRow(
|
||||||
|
icon: Icons.delete_outline_rounded,
|
||||||
|
label: '${_camion.ruta} · ${_camion.tiempoActualizacion}',
|
||||||
|
value: 'Camión a ${_distanciaMetros.toStringAsFixed(0)} m',
|
||||||
|
trailing: w.StatusBadge.amber('~$_minutosEstimados min'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Casa
|
||||||
|
w.InfoRow(
|
||||||
|
icon: Icons.home_outlined,
|
||||||
|
label: _casa.direccionCompleta,
|
||||||
|
value: _casa.alias,
|
||||||
|
trailing: w.StatusBadge.green('Activa'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Barra de progreso de llegada
|
||||||
|
_ArrivalBar(
|
||||||
|
distanciaActual: _distanciaMetros,
|
||||||
|
distanciaTotal: 1000,
|
||||||
|
minutos: _minutosEstimados,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Badge "En vivo" ───────────────────────────────────────────────────────────
|
||||||
|
class _LiveBadge extends StatefulWidget {
|
||||||
|
final bool activo;
|
||||||
|
const _LiveBadge({required this.activo});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_LiveBadge> createState() => _LiveBadgeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LiveBadgeState extends State<_LiveBadge>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _anim;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_anim = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 900),
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_anim.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: AppTheme.softShadow,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _anim,
|
||||||
|
builder: (_, __) => Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: widget.activo
|
||||||
|
? AppTheme.primary.withValues(alpha: 0.5 + _anim.value * 0.5)
|
||||||
|
: AppTheme.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Text(
|
||||||
|
widget.activo ? 'En vivo' : 'Sin servicio',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: widget.activo ? AppTheme.primary : AppTheme.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Barra de llegada estimada ─────────────────────────────────────────────────
|
||||||
|
class _ArrivalBar extends StatelessWidget {
|
||||||
|
final double distanciaActual;
|
||||||
|
final double distanciaTotal;
|
||||||
|
final int minutos;
|
||||||
|
|
||||||
|
const _ArrivalBar({
|
||||||
|
required this.distanciaActual,
|
||||||
|
required this.distanciaTotal,
|
||||||
|
required this.minutos,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final progreso =
|
||||||
|
((distanciaTotal - distanciaActual) / distanciaTotal).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryLight,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
border: Border.all(color: AppTheme.primaryMid, width: 0.5),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text('Llegada estimada',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppTheme.primaryDark)),
|
||||||
|
const Spacer(),
|
||||||
|
Text('~$minutos min',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppTheme.primary)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progreso,
|
||||||
|
backgroundColor: AppTheme.primaryMid.withValues(alpha: 0.4),
|
||||||
|
valueColor:
|
||||||
|
const AlwaysStoppedAnimation<Color>(AppTheme.primary),
|
||||||
|
minHeight: 6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: const [
|
||||||
|
Text('Ahora',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10, color: AppTheme.primaryDark)),
|
||||||
|
Spacer(),
|
||||||
|
Text('Tu casa',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10, color: AppTheme.primaryDark)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
233
views_v1/profile_screen.dart
Normal file
233
views_v1/profile_screen.dart
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
import '../models/models.dart';
|
||||||
|
import '../widgets/widgets.dart' as w;
|
||||||
|
import 'splash_screen.dart';
|
||||||
|
|
||||||
|
class ProfileScreen extends StatelessWidget {
|
||||||
|
const ProfileScreen({super.key});
|
||||||
|
|
||||||
|
final UserModel _usuario = const UserModel(
|
||||||
|
id: 'user-01',
|
||||||
|
nombre: 'Carlos',
|
||||||
|
apellido: 'Martínez',
|
||||||
|
email: 'carlos@ejemplo.com',
|
||||||
|
telefono: '+52 461 123 4567',
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppTheme.background,
|
||||||
|
appBar: AppBar(title: const Text('Mi perfil')),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
// ── Avatar y datos ─────────────────────────────────────────
|
||||||
|
_ProfileHeader(usuario: _usuario),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// ── Mi cuenta ──────────────────────────────────────────────
|
||||||
|
w.SectionTitle(title: 'Mi cuenta'),
|
||||||
|
w.MenuTile(
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
title: 'Editar perfil',
|
||||||
|
subtitle: '${_usuario.nombre} ${_usuario.apellido}',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
w.MenuTile(
|
||||||
|
icon: Icons.lock_outline,
|
||||||
|
title: 'Cambiar contraseña',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
w.MenuTile(
|
||||||
|
icon: Icons.phone_outlined,
|
||||||
|
title: 'Teléfono',
|
||||||
|
subtitle: _usuario.telefono,
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── Configuración ──────────────────────────────────────────
|
||||||
|
w.SectionTitle(title: 'Configuración'),
|
||||||
|
w.MenuTile(
|
||||||
|
icon: Icons.calendar_month_outlined,
|
||||||
|
title: 'Horario del camión',
|
||||||
|
subtitle: 'Ruta Norte · Celaya',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
w.MenuTile(
|
||||||
|
icon: Icons.language_outlined,
|
||||||
|
title: 'Idioma',
|
||||||
|
subtitle: 'Español',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
w.MenuTile(
|
||||||
|
icon: Icons.dark_mode_outlined,
|
||||||
|
title: 'Tema',
|
||||||
|
subtitle: 'Claro',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── Soporte ────────────────────────────────────────────────
|
||||||
|
w.SectionTitle(title: 'Soporte'),
|
||||||
|
w.MenuTile(
|
||||||
|
icon: Icons.help_outline,
|
||||||
|
title: 'Ayuda y preguntas frecuentes',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
w.MenuTile(
|
||||||
|
icon: Icons.bug_report_outlined,
|
||||||
|
title: 'Reportar un problema',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
w.MenuTile(
|
||||||
|
icon: Icons.info_outline,
|
||||||
|
title: 'Acerca de la app',
|
||||||
|
subtitle: 'Versión 1.0.0',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── Cerrar sesión ──────────────────────────────────────────
|
||||||
|
w.MenuTile(
|
||||||
|
icon: Icons.logout_rounded,
|
||||||
|
title: 'Cerrar sesión',
|
||||||
|
iconColor: AppTheme.danger,
|
||||||
|
titleColor: AppTheme.danger,
|
||||||
|
trailing: const SizedBox.shrink(),
|
||||||
|
onTap: () => _confirmarCerrarSesion(context),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'RutaVerde v1.0.0\nServicio de Limpia · Celaya, Gto.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12, color: AppTheme.textHint, height: 1.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _confirmarCerrarSesion(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
backgroundColor: AppTheme.surface,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg)),
|
||||||
|
title: const Text('Cerrar sesión',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppTheme.textPrimary)),
|
||||||
|
content: const Text(
|
||||||
|
'¿Estás seguro de que deseas cerrar sesión?',
|
||||||
|
style: TextStyle(fontSize: 14, color: AppTheme.textSecondary),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
style:
|
||||||
|
TextButton.styleFrom(foregroundColor: AppTheme.textSecondary),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const SplashScreen()),
|
||||||
|
(_) => false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(foregroundColor: AppTheme.danger),
|
||||||
|
child: const Text('Cerrar sesión',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Encabezado de perfil ──────────────────────────────────────────────────────
|
||||||
|
class _ProfileHeader extends StatelessWidget {
|
||||||
|
final UserModel usuario;
|
||||||
|
const _ProfileHeader({required this.usuario});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
border: Border.all(color: AppTheme.border, width: 0.5),
|
||||||
|
boxShadow: AppTheme.softShadow,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Avatar con iniciales
|
||||||
|
Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryLight,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: AppTheme.primaryMid, width: 1.5),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
usuario.iniciales,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppTheme.primaryDark),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
usuario.nombreCompleto,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppTheme.textPrimary),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
usuario.email,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13, color: AppTheme.textSecondary),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
w.StatusBadge.green('Cuenta activa'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit_outlined,
|
||||||
|
color: AppTheme.primary, size: 20),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
541
views_v1/register_screen.dart
Normal file
541
views_v1/register_screen.dart
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
import '../widgets/widgets.dart' as w;
|
||||||
|
import 'main_shell.dart';
|
||||||
|
|
||||||
|
class RegisterScreen extends StatefulWidget {
|
||||||
|
const RegisterScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RegisterScreen> createState() => _RegisterScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RegisterScreenState extends State<RegisterScreen> {
|
||||||
|
final _pageController = PageController();
|
||||||
|
int _currentPage = 0;
|
||||||
|
bool _loading = false;
|
||||||
|
|
||||||
|
// Paso 1
|
||||||
|
final _nombreCtrl = TextEditingController();
|
||||||
|
final _apellidoCtrl = TextEditingController();
|
||||||
|
final _emailCtrl = TextEditingController();
|
||||||
|
final _telefonoCtrl = TextEditingController();
|
||||||
|
final _passCtrl = TextEditingController();
|
||||||
|
bool _obscurePass = true;
|
||||||
|
|
||||||
|
// Paso 2
|
||||||
|
final _calleCtrl = TextEditingController();
|
||||||
|
final _coloniaCtrl = TextEditingController();
|
||||||
|
final _cpCtrl = TextEditingController();
|
||||||
|
int _radioAlerta = 200;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController.dispose();
|
||||||
|
_nombreCtrl.dispose(); _apellidoCtrl.dispose();
|
||||||
|
_emailCtrl.dispose(); _telefonoCtrl.dispose(); _passCtrl.dispose();
|
||||||
|
_calleCtrl.dispose(); _coloniaCtrl.dispose(); _cpCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _nextPage() {
|
||||||
|
_pageController.nextPage(
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
setState(() => _currentPage = 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _register() async {
|
||||||
|
setState(() => _loading = true);
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _loading = false);
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const MainShell()),
|
||||||
|
(_) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppTheme.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
iconTheme: const IconThemeData(color: AppTheme.textPrimary),
|
||||||
|
title: Text(
|
||||||
|
_currentPage == 0 ? 'Crear cuenta' : 'Mi dirección',
|
||||||
|
style: const TextStyle(color: AppTheme.textPrimary, fontSize: 16),
|
||||||
|
),
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(4),
|
||||||
|
child: _StepIndicator(current: _currentPage, total: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: PageView(
|
||||||
|
controller: _pageController,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
_Step1(
|
||||||
|
nombreCtrl: _nombreCtrl,
|
||||||
|
apellidoCtrl: _apellidoCtrl,
|
||||||
|
emailCtrl: _emailCtrl,
|
||||||
|
telefonoCtrl: _telefonoCtrl,
|
||||||
|
passCtrl: _passCtrl,
|
||||||
|
obscurePass: _obscurePass,
|
||||||
|
onTogglePass: () => setState(() => _obscurePass = !_obscurePass),
|
||||||
|
onNext: _nextPage,
|
||||||
|
),
|
||||||
|
_Step2(
|
||||||
|
calleCtrl: _calleCtrl,
|
||||||
|
coloniaCtrl: _coloniaCtrl,
|
||||||
|
cpCtrl: _cpCtrl,
|
||||||
|
radioAlerta: _radioAlerta,
|
||||||
|
onRadioChanged: (v) => setState(() => _radioAlerta = v),
|
||||||
|
onRegister: _register,
|
||||||
|
loading: _loading,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Indicador de pasos ────────────────────────────────────────────────────────
|
||||||
|
class _StepIndicator extends StatelessWidget {
|
||||||
|
final int current;
|
||||||
|
final int total;
|
||||||
|
|
||||||
|
const _StepIndicator({required this.current, required this.total});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
|
||||||
|
child: Row(
|
||||||
|
children: List.generate(total, (i) {
|
||||||
|
final active = i <= current;
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: EdgeInsets.only(right: i < total - 1 ? 6 : 0),
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: active ? AppTheme.primary : AppTheme.border,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Paso 1: Datos personales ──────────────────────────────────────────────────
|
||||||
|
class _Step1 extends StatelessWidget {
|
||||||
|
final TextEditingController nombreCtrl, apellidoCtrl, emailCtrl,
|
||||||
|
telefonoCtrl, passCtrl;
|
||||||
|
final bool obscurePass;
|
||||||
|
final VoidCallback onTogglePass;
|
||||||
|
final VoidCallback onNext;
|
||||||
|
|
||||||
|
const _Step1({
|
||||||
|
required this.nombreCtrl, required this.apellidoCtrl,
|
||||||
|
required this.emailCtrl, required this.telefonoCtrl,
|
||||||
|
required this.passCtrl, required this.obscurePass,
|
||||||
|
required this.onTogglePass, required this.onNext,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
|
// ── Sección personal ──────────────────────────────────────────
|
||||||
|
_FormCard(
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
title: 'Información personal',
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: w.FormField(
|
||||||
|
label: 'Nombre',
|
||||||
|
hint: 'Carlos',
|
||||||
|
controller: nombreCtrl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: w.FormField(
|
||||||
|
label: 'Apellido',
|
||||||
|
hint: 'Martínez',
|
||||||
|
controller: apellidoCtrl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
w.FormField(
|
||||||
|
label: 'Correo electrónico',
|
||||||
|
hint: 'tu@correo.com',
|
||||||
|
controller: emailCtrl,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
w.FormField(
|
||||||
|
label: 'Teléfono',
|
||||||
|
hint: '+52 461 123 4567',
|
||||||
|
controller: telefonoCtrl,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
w.FormField(
|
||||||
|
label: 'Contraseña',
|
||||||
|
hint: '••••••••',
|
||||||
|
controller: passCtrl,
|
||||||
|
obscureText: obscurePass,
|
||||||
|
suffix: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
obscurePass
|
||||||
|
? Icons.visibility_outlined
|
||||||
|
: Icons.visibility_off_outlined,
|
||||||
|
size: 18, color: AppTheme.textSecondary,
|
||||||
|
),
|
||||||
|
onPressed: onTogglePass,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 52,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: onNext,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: const [
|
||||||
|
Text('Siguiente'),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Icon(Icons.arrow_forward, size: 18),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('¿Ya tienes cuenta? ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: AppTheme.textSecondary)),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => Navigator.pop(context),
|
||||||
|
child: const Text('Inicia sesión',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppTheme.primary)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Paso 2: Dirección ─────────────────────────────────────────────────────────
|
||||||
|
class _Step2 extends StatelessWidget {
|
||||||
|
final TextEditingController calleCtrl, coloniaCtrl, cpCtrl;
|
||||||
|
final int radioAlerta;
|
||||||
|
final ValueChanged<int> onRadioChanged;
|
||||||
|
final VoidCallback onRegister;
|
||||||
|
final bool loading;
|
||||||
|
|
||||||
|
const _Step2({
|
||||||
|
required this.calleCtrl, required this.coloniaCtrl, required this.cpCtrl,
|
||||||
|
required this.radioAlerta, required this.onRadioChanged,
|
||||||
|
required this.onRegister, required this.loading,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
|
_FormCard(
|
||||||
|
icon: Icons.home_outlined,
|
||||||
|
title: 'Dirección de tu casa',
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
w.FormField(
|
||||||
|
label: 'Calle y número',
|
||||||
|
hint: 'Av. Insurgentes 245',
|
||||||
|
controller: calleCtrl,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: w.FormField(
|
||||||
|
label: 'Colonia',
|
||||||
|
hint: 'Centro',
|
||||||
|
controller: coloniaCtrl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: w.FormField(
|
||||||
|
label: 'C.P.',
|
||||||
|
hint: '38000',
|
||||||
|
controller: cpCtrl,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
// Usar ubicación actual
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 11, horizontal: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryLight,
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppTheme.primaryMid, width: 0.5),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.my_location,
|
||||||
|
color: AppTheme.primary, size: 18),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Usar mi ubicación actual',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppTheme.primaryDark)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
_FormCard(
|
||||||
|
icon: Icons.notifications_outlined,
|
||||||
|
title: 'Distancia de alerta',
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Te avisamos cuando el camión esté a esta distancia de tu casa.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: AppTheme.textSecondary,
|
||||||
|
height: 1.4),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
...([200, 400, 600]).map((dist) => _RadioOption(
|
||||||
|
value: dist,
|
||||||
|
groupValue: radioAlerta,
|
||||||
|
label: '$dist metros',
|
||||||
|
sublabel: dist == 200
|
||||||
|
? 'Alerta muy temprana (~2-3 min)'
|
||||||
|
: dist == 400
|
||||||
|
? 'Alerta temprana (~4-5 min)'
|
||||||
|
: 'Alerta anticipada (~6-8 min)',
|
||||||
|
onChanged: onRadioChanged,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 52,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: loading ? null : onRegister,
|
||||||
|
child: loading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20, height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2, color: Colors.white),
|
||||||
|
)
|
||||||
|
: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check, size: 18),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Registrarme'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'Al registrarte aceptas los Términos de Servicio\ny la Política de Privacidad.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11, color: AppTheme.textSecondary, height: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tarjeta de formulario ─────────────────────────────────────────────────────
|
||||||
|
class _FormCard extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _FormCard(
|
||||||
|
{required this.icon, required this.title, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
border: Border.all(color: AppTheme.border, width: 0.5),
|
||||||
|
boxShadow: AppTheme.softShadow,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: AppTheme.primary, size: 18),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppTheme.textPrimary)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Opción radio ──────────────────────────────────────────────────────────────
|
||||||
|
class _RadioOption extends StatelessWidget {
|
||||||
|
final int value;
|
||||||
|
final int groupValue;
|
||||||
|
final String label;
|
||||||
|
final String sublabel;
|
||||||
|
final ValueChanged<int> onChanged;
|
||||||
|
|
||||||
|
const _RadioOption({
|
||||||
|
required this.value, required this.groupValue,
|
||||||
|
required this.label, required this.sublabel, required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final selected = value == groupValue;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onChanged(value),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected ? AppTheme.primaryLight : AppTheme.background,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
border: Border.all(
|
||||||
|
color: selected ? AppTheme.primary : AppTheme.border,
|
||||||
|
width: selected ? 1.5 : 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: selected ? AppTheme.primary : AppTheme.border,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: selected
|
||||||
|
? Center(
|
||||||
|
child: Container(
|
||||||
|
width: 8, height: 8,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: AppTheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: selected
|
||||||
|
? AppTheme.primaryDark
|
||||||
|
: AppTheme.textPrimary)),
|
||||||
|
Text(sublabel,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: selected
|
||||||
|
? AppTheme.primary
|
||||||
|
: AppTheme.textSecondary)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
237
views_v1/splash_screen.dart
Normal file
237
views_v1/splash_screen.dart
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
import 'login_screen.dart';
|
||||||
|
import 'register_screen.dart';
|
||||||
|
|
||||||
|
class SplashScreen extends StatefulWidget {
|
||||||
|
const SplashScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SplashScreen> createState() => _SplashScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SplashScreenState extends State<SplashScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _fadeIn;
|
||||||
|
late Animation<Offset> _slideUp;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 900),
|
||||||
|
);
|
||||||
|
_fadeIn = Tween<double>(begin: 0, end: 1).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
_slideUp = Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.3),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
|
||||||
|
|
||||||
|
_controller.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [AppTheme.primary, AppTheme.primaryDark],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 28),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Spacer(flex: 2),
|
||||||
|
|
||||||
|
// ── Ícono de la app ─────────────────────────────────────
|
||||||
|
FadeTransition(
|
||||||
|
opacity: _fadeIn,
|
||||||
|
child: Container(
|
||||||
|
width: 90,
|
||||||
|
height: 90,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withValues(alpha: 0.15),
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppTheme.radiusXl),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.delete_outline_rounded,
|
||||||
|
size: 46,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// ── Nombre y descripción ────────────────────────────────
|
||||||
|
SlideTransition(
|
||||||
|
position: _slideUp,
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: _fadeIn,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'RutaVerde',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: -0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'Sigue en tiempo real el camión de basura\ny recibe alertas cuando esté cerca.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: Colors.white.withValues(alpha: 0.82),
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const Spacer(flex: 3),
|
||||||
|
|
||||||
|
// ── Características rápidas ─────────────────────────────
|
||||||
|
FadeTransition(
|
||||||
|
opacity: _fadeIn,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
_FeatureChip(
|
||||||
|
icon: Icons.location_on_outlined,
|
||||||
|
label: 'Rastreo en vivo',
|
||||||
|
),
|
||||||
|
_FeatureChip(
|
||||||
|
icon: Icons.notifications_outlined,
|
||||||
|
label: 'Alertas',
|
||||||
|
),
|
||||||
|
_FeatureChip(
|
||||||
|
icon: Icons.home_outlined,
|
||||||
|
label: 'Tu dirección',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
// ── Botones ─────────────────────────────────────────────
|
||||||
|
FadeTransition(
|
||||||
|
opacity: _fadeIn,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: AppTheme.primaryDark,
|
||||||
|
minimumSize: const Size(double.infinity, 52),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
),
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const RegisterScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Crear cuenta'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const LoginScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Ya tengo cuenta'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Servicio de Limpia · Celaya, Gto.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.white.withValues(alpha: 0.45),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FeatureChip extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
const _FeatureChip({required this.icon, required this.label});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
border: Border.all(color: Colors.white.withValues(alpha: 0.2)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: Colors.white, size: 22),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user