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>

implementacion de login, vistas, correcion de errores en vista registro, domicilios
This commit is contained in:
shinra32
2026-05-22 23:07:24 -06:00
parent b4ee3e7b49
commit c91b6e2091
52 changed files with 3940 additions and 4368 deletions

View File

@@ -0,0 +1,489 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_widgets.dart';
import '../../core/network/api_client.dart';
// ── Provider de ETA ───────────────────────────────────────────────────────────
final etaProvider = FutureProvider.autoDispose<_EtaResult>((ref) async {
final dio = ref.read(apiClientProvider);
final addressesResp = await dio.get<dynamic>('/addresses');
final raw = addressesResp.data;
List<dynamic> items = const [];
if (raw is List) {
items = raw;
} else if (raw is Map && raw['data'] is List) {
items = raw['data'] as List;
} else if (raw is Map && raw['addresses'] is List) {
items = raw['addresses'] as List;
}
if (items.isEmpty) {
return const _EtaResult.noAddress();
}
final addressId = items.first['id'] as String;
final etaResp = await dio.get<dynamic>(
'/eta',
queryParameters: {'address_id': addressId},
);
final data = etaResp.data as Map<String, dynamic>;
return _EtaResult(
mensaje: data['mensaje'] as String? ?? '',
status: data['status'] as String? ?? '',
direccion: items.first['calle'] as String? ?? '',
colonia: items.first['colonia'] as String? ?? '',
hasAddress: true,
);
});
class _EtaResult {
final String mensaje;
final String status;
final String direccion;
final String colonia;
final bool hasAddress;
const _EtaResult({
required this.mensaje,
required this.status,
required this.direccion,
required this.colonia,
required this.hasAddress,
});
const _EtaResult.noAddress()
: mensaje = '',
status = '',
direccion = '',
colonia = '',
hasAddress = false;
double get progreso {
if (mensaje.contains('15 minutos') || mensaje.contains('Está atendiendo')) {
return 0.85;
}
if (mensaje.contains('finalizado')) return 1.0;
return 0.35;
}
String get etiquetaEstado {
if (status == 'completada') return 'Finalizado';
if (status == 'en_ruta') return 'En ruta';
return 'Pendiente';
}
}
// ── Pantalla ETA ──────────────────────────────────────────────────────────────
class EtaScreen extends ConsumerWidget {
const EtaScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final etaAsync = ref.watch(etaProvider);
return Scaffold(
backgroundColor: AppTheme.background,
appBar: AppBar(
title: const Text('Estado del camión'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Actualizar',
onPressed: () => ref.invalidate(etaProvider),
),
],
),
body: etaAsync.when(
loading: () => const _EtaLoading(),
error: (error, _) => _EtaError(
error: error.toString(),
onRetry: () => ref.invalidate(etaProvider),
),
data: (result) => result.hasAddress
? _EtaContent(result: result)
: _NoAddressState(
onAdd: () => context.go('/addresses/new'),
),
),
);
}
}
// ── Contenido ETA ─────────────────────────────────────────────────────────────
class _EtaContent extends StatelessWidget {
final _EtaResult result;
const _EtaContent({required this.result});
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
// ── Tarjeta de estado principal ────────────────────────────────
Container(
padding: const EdgeInsets.all(20),
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: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppTheme.primary,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.delete_outline_rounded,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Camión recolector',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppTheme.primaryDark,
),
),
const SizedBox(height: 2),
AppStatusBadge.green(result.etiquetaEstado),
],
),
),
_LiveDot(active: result.status == 'en_ruta'),
],
),
const SizedBox(height: 20),
// Mensaje ETA
Text(
result.mensaje,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: AppTheme.primaryDark,
height: 1.3,
),
),
const SizedBox(height: 16),
// Barra de progreso
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: result.progreso,
backgroundColor:
AppTheme.primaryMid.withValues(alpha: 0.35),
valueColor:
const AlwaysStoppedAnimation<Color>(AppTheme.primary),
minHeight: 8,
),
),
const SizedBox(height: 6),
const Row(
children: [
Text('Inicio de ruta',
style: TextStyle(
fontSize: 10, color: AppTheme.primaryDark)),
Spacer(),
Text('Tu casa',
style: TextStyle(
fontSize: 10, color: AppTheme.primaryDark)),
],
),
],
),
),
const SizedBox(height: 16),
// ── Domicilio registrado ───────────────────────────────────────
AppInfoRow(
icon: Icons.home_outlined,
label: 'Col. ${result.colonia}',
value: result.direccion.isEmpty ? 'Mi domicilio' : result.direccion,
trailing: AppStatusBadge.green('Activo'),
),
const SizedBox(height: 16),
// ── Aviso de privacidad ────────────────────────────────────────
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.blueLight,
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.shield_outlined, color: AppTheme.blue, size: 18),
SizedBox(width: 10),
Expanded(
child: Text(
'Tu ubicación exacta y la del camión no se comparten. Solo ves el estado de tu ruta.',
style: TextStyle(
fontSize: 12, color: AppTheme.blue, height: 1.5),
),
),
],
),
),
const SizedBox(height: 16),
// ── Horario estimado de la semana ──────────────────────────────
AppSectionTitle(title: 'Horario del camión'),
_HorarioCard(),
],
);
}
}
// ── Punto animado "en vivo" ───────────────────────────────────────────────────
class _LiveDot extends StatefulWidget {
final bool active;
const _LiveDot({required this.active});
@override
State<_LiveDot> createState() => _LiveDotState();
}
class _LiveDotState extends State<_LiveDot>
with SingleTickerProviderStateMixin {
late final 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) {
if (!widget.active) {
return const SizedBox.shrink();
}
return AnimatedBuilder(
animation: _anim,
builder: (_, child) => Container(
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.primary
.withValues(alpha: 0.5 + _anim.value * 0.5),
),
),
);
}
}
// ── Horario ───────────────────────────────────────────────────────────────────
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});
}
// ── Sin domicilio ─────────────────────────────────────────────────────────────
class _NoAddressState extends StatelessWidget {
final VoidCallback onAdd;
const _NoAddressState({required this.onAdd});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: const BoxDecoration(
color: AppTheme.primaryLight,
shape: BoxShape.circle,
),
child: const Icon(Icons.home_outlined,
color: AppTheme.primary, size: 40),
),
const SizedBox(height: 20),
const Text(
'Sin domicilio registrado',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary),
),
const SizedBox(height: 8),
const Text(
'Registra tu domicilio para\nrecibir el ETA de tu ruta.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13, color: AppTheme.textSecondary, height: 1.5),
),
const SizedBox(height: 24),
SizedBox(
width: 200,
child: ElevatedButton(
onPressed: onAdd,
child: const Text('Agregar domicilio'),
),
),
],
),
),
);
}
}
// ── Cargando ──────────────────────────────────────────────────────────────────
class _EtaLoading extends StatelessWidget {
const _EtaLoading();
@override
Widget build(BuildContext context) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: AppTheme.primary),
SizedBox(height: 16),
Text('Consultando estado del camión…',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 14)),
],
),
);
}
}
// ── Error ─────────────────────────────────────────────────────────────────────
class _EtaError extends StatelessWidget {
final String error;
final VoidCallback onRetry;
const _EtaError({required this.error, required this.onRetry});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.wifi_off_outlined,
color: AppTheme.textSecondary, size: 48),
const SizedBox(height: 16),
const Text('No se pudo obtener el estado',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary)),
const SizedBox(height: 8),
Text(error,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary)),
const SizedBox(height: 20),
SizedBox(
width: 160,
child: ElevatedButton(
onPressed: onRetry,
child: const Text('Reintentar'),
),
),
],
),
),
);
}
}