Files
hackathon-innovaflow5.0-cdf…/recolecta_app/lib/features/eta/eta_screen.dart
shinra32 c91b6e2091 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
2026-05-22 23:07:24 -06:00

490 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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