Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>

Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com>
Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com>

primeras vistas para el frontend, configuracion para firebase
This commit is contained in:
shinra32
2026-05-22 17:45:54 -06:00
parent ba5e5ea12c
commit 9e6bd04755
26 changed files with 322 additions and 108 deletions

45
views/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
views/.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- platform: windows
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
views/README.md Normal file
View File

@@ -0,0 +1,3 @@
# rutaverde
A new Flutter project.

View File

@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

33
views/lib/main.dart Normal file
View File

@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'theme/app_theme.dart';
import 'screens/splash_screen.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
),
);
runApp(const RutaVerdeApp());
}
class RutaVerdeApp extends StatelessWidget {
const RutaVerdeApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'RutaVerde',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
home: const SplashScreen(),
);
}
}

View File

@@ -0,0 +1,196 @@
// ── Usuario ──────────────────────────────────────────────────────────────────
class UserModel {
final String id;
final String nombre;
final String apellido;
final String email;
final String telefono;
final List<HouseModel> casas;
const UserModel({
required this.id,
required this.nombre,
required this.apellido,
required this.email,
required this.telefono,
this.casas = const [],
});
String get nombreCompleto => '$nombre $apellido';
String get iniciales =>
'${nombre.isNotEmpty ? nombre[0] : ''}${apellido.isNotEmpty ? apellido[0] : ''}'
.toUpperCase();
UserModel copyWith({
String? nombre,
String? apellido,
String? email,
String? telefono,
List<HouseModel>? casas,
}) {
return UserModel(
id: id,
nombre: nombre ?? this.nombre,
apellido: apellido ?? this.apellido,
email: email ?? this.email,
telefono: telefono ?? this.telefono,
casas: casas ?? this.casas,
);
}
}
// ── Casa ─────────────────────────────────────────────────────────────────────
class HouseModel {
final String id;
final String alias;
final String calle;
final String colonia;
final String codigoPostal;
final double latitud;
final double longitud;
final int radioAlertaMetros;
final bool alertaCercana;
final bool alertaMedia;
final bool recordatorioDiario;
final bool activa;
const HouseModel({
required this.id,
this.alias = 'Casa principal',
required this.calle,
required this.colonia,
required this.codigoPostal,
required this.latitud,
required this.longitud,
this.radioAlertaMetros = 200,
this.alertaCercana = true,
this.alertaMedia = false,
this.recordatorioDiario = true,
this.activa = true,
});
String get direccionCompleta => '$calle, Col. $colonia, C.P. $codigoPostal';
HouseModel copyWith({
String? alias,
String? calle,
String? colonia,
String? codigoPostal,
double? latitud,
double? longitud,
int? radioAlertaMetros,
bool? alertaCercana,
bool? alertaMedia,
bool? recordatorioDiario,
bool? activa,
}) {
return HouseModel(
id: id,
alias: alias ?? this.alias,
calle: calle ?? this.calle,
colonia: colonia ?? this.colonia,
codigoPostal: codigoPostal ?? this.codigoPostal,
latitud: latitud ?? this.latitud,
longitud: longitud ?? this.longitud,
radioAlertaMetros: radioAlertaMetros ?? this.radioAlertaMetros,
alertaCercana: alertaCercana ?? this.alertaCercana,
alertaMedia: alertaMedia ?? this.alertaMedia,
recordatorioDiario: recordatorioDiario ?? this.recordatorioDiario,
activa: activa ?? this.activa,
);
}
}
// ── Camión ───────────────────────────────────────────────────────────────────
class TruckLocation {
final String id;
final String ruta;
final double latitud;
final double longitud;
final DateTime ultimaActualizacion;
final bool enServicio;
const TruckLocation({
required this.id,
required this.ruta,
required this.latitud,
required this.longitud,
required this.ultimaActualizacion,
this.enServicio = true,
});
String get tiempoActualizacion {
final diff = DateTime.now().difference(ultimaActualizacion);
if (diff.inSeconds < 60) return 'Hace ${diff.inSeconds} s';
if (diff.inMinutes < 60) return 'Hace ${diff.inMinutes} min';
return 'Hace ${diff.inHours} h';
}
}
// ── Alerta ───────────────────────────────────────────────────────────────────
enum TipoAlerta { cercana, media, recordatorio }
class AlertaModel {
final String id;
final TipoAlerta tipo;
final double distanciaMetros;
final DateTime fecha;
final String direccionCasa;
final bool leida;
const AlertaModel({
required this.id,
required this.tipo,
required this.distanciaMetros,
required this.fecha,
required this.direccionCasa,
this.leida = false,
});
String get distanciaTexto {
if (distanciaMetros < 1000) {
return '${distanciaMetros.toStringAsFixed(0)} m';
}
return '${(distanciaMetros / 1000).toStringAsFixed(1)} km';
}
String get tiempoEstimadoTexto {
// ~5 km/h velocidad promedio del camión
final segundos = (distanciaMetros / (5000 / 3600)).round();
if (segundos < 60) return 'Menos de 1 min';
final minutos = (segundos / 60).ceil();
return '~$minutos min';
}
String get fechaFormateada {
final ahora = DateTime.now();
final hoy = DateTime(ahora.year, ahora.month, ahora.day);
final fechaDia = DateTime(fecha.year, fecha.month, fecha.day);
if (fechaDia == hoy) {
return 'Hoy, ${_formatHora(fecha)}';
}
final ayer = hoy.subtract(const Duration(days: 1));
if (fechaDia == ayer) return 'Ayer, ${_formatHora(fecha)}';
const dias = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
const meses = ['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic'];
return '${dias[fecha.weekday - 1]} ${fecha.day} ${meses[fecha.month - 1]}, ${_formatHora(fecha)}';
}
String get etiquetaFecha {
final ahora = DateTime.now();
final hoy = DateTime(ahora.year, ahora.month, ahora.day);
final fechaDia = DateTime(fecha.year, fecha.month, fecha.day);
if (fechaDia == hoy) return 'Hoy';
const dias = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
return dias[fecha.weekday - 1];
}
String _formatHora(DateTime dt) {
final h = dt.hour > 12 ? dt.hour - 12 : dt.hour == 0 ? 12 : dt.hour;
final m = dt.minute.toString().padLeft(2, '0');
final ampm = dt.hour >= 12 ? 'p.m.' : 'a.m.';
return '$h:$m $ampm';
}
}

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

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

View File

@@ -0,0 +1,244 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
import '../widgets/widgets.dart' as w;
import 'main_shell.dart';
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();
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: (_) => 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: 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: 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)),
],
),
),
);
}
}

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

View 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.withOpacity(0.08),
strokeColor: AppTheme.blue.withOpacity(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.withOpacity(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.withOpacity(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)),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,234 @@
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: () {},
),
],
),
);
}
}

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

View 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.withOpacity(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.withOpacity(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.withOpacity(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.withOpacity(0.12),
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
border: Border.all(color: Colors.white.withOpacity(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,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
class AppTheme {
// ── Colores principales ──────────────────────────────────────────────────
static const Color primary = Color(0xFF1D9E75);
static const Color primaryDark = Color(0xFF0F6E56);
static const Color primaryLight = Color(0xFFE1F5EE);
static const Color primaryMid = Color(0xFF9FE1CB);
static const Color blue = Color(0xFF185FA5);
static const Color blueLight = Color(0xFFE6F1FB);
static const Color amber = Color(0xFF854F0B);
static const Color amberLight = Color(0xFFFAEEDA);
static const Color danger = Color(0xFFE24B4A);
static const Color dangerLight = Color(0xFFFCEBEB);
static const Color textPrimary = Color(0xFF1A1A1A);
static const Color textSecondary = Color(0xFF6B7280);
static const Color textHint = Color(0xFFAAAAAA);
static const Color surface = Color(0xFFFFFFFF);
static const Color background = Color(0xFFF5F7F5);
static const Color border = Color(0xFFE5E7EB);
static const Color borderLight = Color(0xFFF0F2F0);
// ── Radios ───────────────────────────────────────────────────────────────
static const double radiusSm = 8.0;
static const double radiusMd = 12.0;
static const double radiusLg = 16.0;
static const double radiusXl = 24.0;
static const double radiusFull = 100.0;
// ── Sombras ──────────────────────────────────────────────────────────────
static List<BoxShadow> get cardShadow => [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 12,
offset: const Offset(0, 4),
),
];
static List<BoxShadow> get softShadow => [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
];
// ── ThemeData ────────────────────────────────────────────────────────────
static ThemeData get lightTheme => ThemeData(
useMaterial3: true,
fontFamily: 'SF Pro Display',
colorScheme: ColorScheme.fromSeed(
seedColor: primary,
primary: primary,
secondary: primaryDark,
surface: surface,
background: background,
),
scaffoldBackgroundColor: background,
appBarTheme: const AppBarTheme(
backgroundColor: primary,
foregroundColor: Colors.white,
elevation: 0,
centerTitle: false,
titleTextStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
elevation: 0,
minimumSize: const Size(double.infinity, 52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
textStyle: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: const BorderSide(color: Colors.white54, width: 1.5),
minimumSize: const Size(double.infinity, 52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
textStyle: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: surface,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: primary, width: 1.5),
),
labelStyle: const TextStyle(color: textSecondary, fontSize: 13),
hintStyle: const TextStyle(color: textHint, fontSize: 13),
),
);
}

View File

@@ -0,0 +1,382 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
// ── Badge de estado ───────────────────────────────────────────────────────────
class StatusBadge extends StatelessWidget {
final String label;
final Color backgroundColor;
final Color textColor;
const StatusBadge({
super.key,
required this.label,
this.backgroundColor = AppTheme.primaryLight,
this.textColor = AppTheme.primaryDark,
});
factory StatusBadge.green(String label) => StatusBadge(
label: label,
backgroundColor: AppTheme.primaryLight,
textColor: AppTheme.primaryDark,
);
factory StatusBadge.amber(String label) => StatusBadge(
label: label,
backgroundColor: AppTheme.amberLight,
textColor: AppTheme.amber,
);
factory StatusBadge.gray(String label) => StatusBadge(
label: label,
backgroundColor: const Color(0xFFF1EFE8),
textColor: const Color(0xFF5F5E5A),
);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: textColor,
),
),
);
}
}
// ── Tarjeta base ──────────────────────────────────────────────────────────────
class AppCard extends StatelessWidget {
final Widget child;
final EdgeInsets? padding;
final Color? borderColor;
final VoidCallback? onTap;
const AppCard({
super.key,
required this.child,
this.padding,
this.borderColor,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: double.infinity,
padding: padding ?? const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
border: Border.all(
color: borderColor ?? AppTheme.border,
width: 0.5,
),
boxShadow: AppTheme.softShadow,
),
child: child,
),
);
}
}
// ── Fila de información con ícono ─────────────────────────────────────────────
class InfoRow extends StatelessWidget {
final IconData icon;
final String label;
final String value;
final Widget? trailing;
const InfoRow({
super.key,
required this.icon,
required this.label,
required this.value,
this.trailing,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
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: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: AppTheme.primary, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary)),
const SizedBox(height: 2),
Text(label,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary)),
],
),
),
if (trailing != null) trailing!,
],
),
);
}
}
// ── Campo de formulario ───────────────────────────────────────────────────────
class FormField extends StatelessWidget {
final String label;
final String? hint;
final TextEditingController? controller;
final bool obscureText;
final TextInputType? keyboardType;
final String? initialValue;
final Widget? suffix;
final int? maxLines;
const FormField({
super.key,
required this.label,
this.hint,
this.controller,
this.obscureText = false,
this.keyboardType,
this.initialValue,
this.suffix,
this.maxLines = 1,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary)),
const SizedBox(height: 6),
TextFormField(
controller: controller,
initialValue: initialValue,
obscureText: obscureText,
keyboardType: keyboardType,
maxLines: maxLines,
style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary),
decoration: InputDecoration(
hintText: hint,
suffixIcon: suffix,
),
),
],
);
}
}
// ── Sección con título ────────────────────────────────────────────────────────
class SectionTitle extends StatelessWidget {
final String title;
final Widget? action;
const SectionTitle({super.key, required this.title, this.action});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
Text(
title.toUpperCase(),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppTheme.textSecondary,
letterSpacing: 0.8,
),
),
const Spacer(),
if (action != null) action!,
],
),
);
}
}
// ── Toggle con label ──────────────────────────────────────────────────────────
class LabeledSwitch extends StatelessWidget {
final String label;
final bool value;
final ValueChanged<bool> onChanged;
const LabeledSwitch({
super.key,
required this.label,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Expanded(
child: Text(label,
style: const TextStyle(
fontSize: 14, color: AppTheme.textPrimary)),
),
Switch.adaptive(
value: value,
onChanged: onChanged,
activeColor: AppTheme.primary,
),
],
),
);
}
}
// ── Ítem de menú ──────────────────────────────────────────────────────────────
class MenuTile extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final VoidCallback? onTap;
final Color? iconColor;
final Color? titleColor;
final Widget? trailing;
const MenuTile({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.onTap,
this.iconColor,
this.titleColor,
this.trailing,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
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: [
Icon(icon,
color: iconColor ?? AppTheme.primary, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: titleColor ?? AppTheme.textPrimary)),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(subtitle!,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary)),
],
],
),
),
trailing ??
const Icon(Icons.chevron_right,
color: AppTheme.textSecondary, size: 18),
],
),
),
);
}
}
// ── Bottom Nav Bar ────────────────────────────────────────────────────────────
class AppBottomNav extends StatelessWidget {
final int currentIndex;
final Function(int) onTap;
const AppBottomNav({
super.key,
required this.currentIndex,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: currentIndex,
onTap: onTap,
type: BottomNavigationBarType.fixed,
backgroundColor: AppTheme.surface,
selectedItemColor: AppTheme.primary,
unselectedItemColor: AppTheme.textSecondary,
selectedFontSize: 11,
unselectedFontSize: 11,
elevation: 12,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.map_outlined),
activeIcon: Icon(Icons.map),
label: 'Mapa',
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications_outlined),
activeIcon: Icon(Icons.notifications),
label: 'Alertas',
),
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Mi casa',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: 'Perfil',
),
],
);
}
}

770
views/pubspec.lock Normal file
View File

@@ -0,0 +1,770 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "8f89e371e2883de35cdc78f648e725fa4da5f3b6c927269f00fa68f1ea92b598"
url: "https://pub.dev"
source: hosted
version: "1.3.71"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
dbus:
dependency: transitive
description:
name: dbus
sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91"
url: "https://pub.dev"
source: hosted
version: "0.7.13"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "93a5bde9775fd5adcc937f39dfa04ae0bc89c4d79bea6abc49de3f7b049d9ff6"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: "4a120366dbf7d5a8ee9438978530b664b855728fb8dcc3a201017660817e555b"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: "7c98f10b8c8e5adedc0b810b66a877120696675e2c22d9ca9caca092da0d9e57"
url: "https://pub.dev"
source: hosted
version: "3.7.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "8d0dc81a31cd030170508dc3e89bfd14355b20a1b991340af5f018e37daab5d7"
url: "https://pub.dev"
source: hosted
version: "16.2.2"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "37abb0b0535c5497605ee94c12470e1ebbbe47e71a22d0c20bffcc912311f8cb"
url: "https://pub.dev"
source: hosted
version: "4.7.11"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "54e22b43e2c26a2728a3f68c188de0f9011993ae19ae959a06d476dad935c776"
url: "https://pub.dev"
source: hosted
version: "4.1.7"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
url: "https://pub.dev"
source: hosted
version: "18.0.1"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
url: "https://pub.dev"
source: hosted
version: "2.0.34"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
geoclue:
dependency: transitive
description:
name: geoclue
sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f
url: "https://pub.dev"
source: hosted
version: "0.1.1"
geolocator:
dependency: "direct main"
description:
name: geolocator
sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516"
url: "https://pub.dev"
source: hosted
version: "14.0.2"
geolocator_android:
dependency: transitive
description:
name: geolocator_android
sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
geolocator_apple:
dependency: transitive
description:
name: geolocator_apple
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
url: "https://pub.dev"
source: hosted
version: "2.3.13"
geolocator_linux:
dependency: transitive
description:
name: geolocator_linux
sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797
url: "https://pub.dev"
source: hosted
version: "0.2.4"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
url: "https://pub.dev"
source: hosted
version: "4.2.6"
geolocator_web:
dependency: transitive
description:
name: geolocator_web
sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172
url: "https://pub.dev"
source: hosted
version: "4.1.3"
geolocator_windows:
dependency: transitive
description:
name: geolocator_windows
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
url: "https://pub.dev"
source: hosted
version: "0.2.5"
google_maps:
dependency: transitive
description:
name: google_maps
sha256: "5d410c32112d7c6eb7858d359275b2aa04778eed3e36c745aeae905fb2fa6468"
url: "https://pub.dev"
source: hosted
version: "8.2.0"
google_maps_flutter:
dependency: "direct main"
description:
name: google_maps_flutter
sha256: fc714bf8072e2c121d4277cb6dca23bbfae954b6c7b5d6dd73f1bc8d09762921
url: "https://pub.dev"
source: hosted
version: "2.17.0"
google_maps_flutter_android:
dependency: transitive
description:
name: google_maps_flutter_android
sha256: f1eb5ffa34ba41f8591e53ce439f78af179a506e8386a1297d0ecd202e05c734
url: "https://pub.dev"
source: hosted
version: "2.19.8"
google_maps_flutter_ios:
dependency: transitive
description:
name: google_maps_flutter_ios
sha256: "5ed8d8d0f93dfa7f5039c409c500948e98e59068f8f6fcf9105bfd07e3709d7f"
url: "https://pub.dev"
source: hosted
version: "2.18.1"
google_maps_flutter_platform_interface:
dependency: transitive
description:
name: google_maps_flutter_platform_interface
sha256: ddbe34435dfb34e83fca295c6a8dcc53c3b51487e9eec3c737ce4ae605574347
url: "https://pub.dev"
source: hosted
version: "2.15.0"
google_maps_flutter_web:
dependency: transitive
description:
name: google_maps_flutter_web
sha256: "9b068070bf18b5ec6a7d8ac512c7d557377dbe267658d264d2095b7ee4f1f6c5"
url: "https://pub.dev"
source: hosted
version: "0.6.2+1"
gsettings:
dependency: transitive
description:
name: gsettings
sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
url: "https://pub.dev"
source: hosted
version: "0.2.8"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http:
dependency: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
package_info_plus:
dependency: transitive
description:
name: package_info_plus
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
url: "https://pub.dev"
source: hosted
version: "9.0.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev"
source: hosted
version: "12.0.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev"
source: hosted
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
sanitize_html:
dependency: transitive
description:
name: sanitize_html
sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
timezone:
dependency: transitive
description:
name: timezone
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
url: "https://pub.dev"
source: hosted
version: "0.10.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
sdks:
dart: ">=3.11.0 <4.0.0"
flutter: ">=3.38.0"

33
views/pubspec.yaml Normal file
View File

@@ -0,0 +1,33 @@
name: rutaverde
description: Rastreo del camión de basura en tiempo real
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.6
google_maps_flutter: ^2.5.0
geolocator: ^14.0.2
flutter_local_notifications: ^18.0.1
firebase_core: ^4.9.0
firebase_messaging: ^16.2.2
provider: ^6.1.1
shared_preferences: ^2.2.2
http: ^1.1.0
intl: ^0.20.2
permission_handler: ^12.0.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
assets:
- assets/images/